国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

Spring Test, JUnit, Mockito, Hamcrest 集成 Web 測試

2019-11-08 03:25:54
字體:
來源:轉載
供稿:網友

關于SPRing 3.2

1. Spring 3.2 及以上版本自動開啟檢測URL后綴,設置Response content-type功能, 如果不手動關閉這個功能,當url后綴與accept頭不一致時, Response的content-type將會和request的accept不一致,導致報406

關閉URL后綴檢測的方法如下

    <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">        <property name="favorPathExtension" value="false" />        <property name="favorParameter" value="false" />    </bean>

 

2. Spring-Test框架無法應用關閉Spring自動URL后綴檢測的設置, 且StandaloneMockMvcBuilder將設置favorPathExtendsion屬性的方法設置為protected

即 關閉自動匹配URL后綴, 忽略Accept頭, 自動設置Reponse Content-Type為 URL后綴類型 的配置, 所以如果要使用Spring-Test測試返回類型為JSON的@ResponseBody API, 必須將請求URL后綴改為.json和accept頭(application/json)相匹配

一個可行的方案是繼承StandaloneMockMvcBuilder, 將其favorPathExtendsion改為false, 這樣既可禁用自動匹配URL后綴功能

 

前言

實際上需要測試一個Spring的MVC controller,主要要做的就是模擬一個真實的Spring的上下文環境, 同時mock出訪問這個MVC方法的request, 并通過斷言去判斷響應及方法內部個方法的調用情況的正確性

 

需要準備的Maven依賴

復制代碼
    <dependencies>        <dependency>            <groupId>org.codehaus.jackson</groupId>            <artifactId>jackson-core-asl</artifactId>            <version>1.9.9</version>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.codehaus.jackson</groupId>            <artifactId>jackson-mapper-asl</artifactId>            <version>1.9.9</version>            <scope>test</scope>        </dependency>        <!-- spring -->        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-webmvc</artifactId>            <version>3.2.4.RELEASE</version>        </dependency>        <!-- servlet -->        <dependency>            <groupId>javax.servlet</groupId>            <artifactId>servlet-api</artifactId>            <version>3.0.1</version>        </dependency>        <dependency>            <groupId>javax.servlet</groupId>            <artifactId>jstl</artifactId>            <version>1.2</version>        </dependency>        <!-- logger -->        <dependency>            <groupId>org.slf4j</groupId>            <artifactId>slf4j-api</artifactId>            <version>1.7.5</version>        </dependency>        <dependency>            <groupId>org.slf4j</groupId>            <artifactId>jcl-over-slf4j</artifactId>            <version>1.7.5</version>        </dependency>        <dependency>            <groupId>ch.qos.logback</groupId>            <artifactId>logback-classic</artifactId>            <version>1.0.13</version>        </dependency>        <!-- test -->        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-test</artifactId>            <version>3.2.4.RELEASE</version>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.mockito</groupId>            <artifactId>mockito-core</artifactId>            <version>1.9.5</version>            <scope>test</scope>            <exclusions>                <exclusion>                    <artifactId>hamcrest-core</artifactId>                    <groupId>org.hamcrest</groupId>                </exclusion>            </exclusions>        </dependency>        <dependency>            <groupId>junit</groupId>            <artifactId>junit</artifactId>            <version>4.11</version>            <scope>test</scope>            <exclusions>                <exclusion>                    <artifactId>hamcrest-core</artifactId>                    <groupId>org.hamcrest</groupId>                </exclusion>            </exclusions>        </dependency>        <dependency>            <groupId>org.hamcrest</groupId>            <artifactId>hamcrest-all</artifactId>            <version>1.3</version>            <scope>test</scope>        </dependency>        <!-- validation -->        <dependency>            <groupId>javax.validation</groupId>            <artifactId>validation-api</artifactId>            <version>1.1.0.Final</version>        </dependency>        <dependency>            <groupId>org.hibernate</groupId>            <artifactId>hibernate-validator</artifactId>            <version>5.0.1.Final</version>        </dependency>    </dependencies>復制代碼

 

 

對轉發到頁面的Controller方法進行測試

Controller

復制代碼
@Controller@RequestMapping("/category")public class CategoryController extends AbstractController {    @Resource    CategoryService categoryService;    /**     * 課程類目管理頁面     *      * @return     */    @RequestMapping("/manage.htm")    public ModelAndView categoryManage() {        List<Category> categoryList = categoryService.fetchAllCategories();        return new ModelAndView("category/categoryList").addObject(categoryList);    }}復制代碼

 

測試類

復制代碼
@WebAppConfiguration@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = { "classpath:spring/access-control.xml", "classpath:spring/dao.xml",        "classpath:spring/property.xml", "classpath:spring/service.xml" })// "file:src/main/webapp/WEB-INF/spring-servlet.xml" })public class CategoryControllerTest {    private MockMvc mockMvc;    @Mock    private CategoryService mockCategoryService;    @InjectMocks    private CategoryController categoryController;    // @Resource    // private WebApplicationContext webApplicationContext;    @Before    public void before() throws Exception {        MockitoAnnotations.initMocks(this); // 初始化mock對象        Mockito.reset(mockCategoryService); // 重置mock對象        /*         * 如果要使用完全默認Spring Web Context, 例如不需要對Controller注入,則使用 WebApplicationContext mockMvc =         * MockMvcBuilders.webAppContextSetup(webApplicationContext).build();         */        // mockMvc = MockMvcBuilders.standaloneSetup(categoryController).build();        mockMvc = QMockMvcBuilders.standaloneSetup(categoryController).build();    }    /**     * 課程分類管理測試     *      * @throws Exception     */    @Test    public void testCategoryManage() throws Exception {        // 構建測試數據        Category c1 = new CategoryBuilder().id(1).name("cat1").build();        Category c2 = new CategoryBuilder().id(2).name("cat2").build();        // 定義方法行為        when(mockCategoryService.fetchAllCategories()).thenReturn(ImmutableList.of(c1, c2));        // 構造http請求及期待響應行為        mockMvc.perform(get("/category/manage.htm"))                .andDo(print()) // 輸出請求和響應信息                .andExpect(status().isOk())                .andExpect(view().name("category/categoryList"))                // .andExpect(forwardedUrl("/WEB-INF/jsp/category/categoryList.jsp"))                .andExpect(model().attribute("categoryList", hasSize(2)))                .andExpect(                        model().attribute("categoryList",                                hasItem(allOf(hasproperty("id", is(1)), hasProperty("name", is("cat1"))))))                .andExpect(                        model().attribute("categoryList",                                hasItem(allOf(hasProperty("id", is(2)), hasProperty("name", is("cat2"))))));        verify(mockCategoryService, times(1)).fetchAllCategories();        verifyNoMoreInteractions(mockCategoryService);    }}復制代碼

下面對各變量進行解釋

@WebAppConfiguration: 表明該類會使用web應用程序的默認根目錄來載入ApplicationContext, 默認的更目錄是"src/main/webapp", 如果需要更改這個更目錄可以修改該注釋的value值

@RunWith: 使用 Spring-Test 框架

@ContextConfiguration(location = ): 指定需要加載的spring配置文件的地址

@Mock: 需要被Mock的對象

@InjectMocks: 需要將Mock對象注入的對象, 此處就是Controller

@Before: 在每次Test方法之前運行的方法

 

特別需要注意的是, MockMvc就是用來模擬我們的MVC環境的對象, 他負責模擬Spring的MVC設置, 例如對Controller方法的RequestMapping等的掃描, 使用什么ViewResolver等等, 一般我們使用默認配置即可

由于此處我們需要將Controller mock掉, 所以我們不能使用真實的Spring MVC環境, 要使用與原web程序一樣的真實的Spring MVC環境, 請使用

MockMvcBuilders.webAppContextSetup(webApplicationContext).build()

此處我們使用自定義的web MVC環境, controller也是自己注入的

        // mockMvc = MockMvcBuilders.standaloneSetup(categoryController).build();        mockMvc = QMockMvcBuilders.standaloneSetup(categoryController).build();

注意這里使用的是QMockMvcBuilders, 而不是mockito提供的MockMvcBuilders, 原因就是Spring3.2 默認開啟的忽略accept, url后綴匹配自動設置response content-type,這樣容易導致406

所以我想把自動關閉后綴匹配, 又由于MockMvcBuilders無法讀取spring-mvc的配置文件, 無法關閉該特性, 且MockMvcBuilders提供的關閉該特性(關閉favorPathExtension屬性)內部方法居然是protected的,所以我只好繼承該類去關閉該特性了

復制代碼
package com.qunar.fresh.exam.web.mockmvc;/** * @author zhenwei.liu created on 2013 13-10-15 上午1:19 * @version 1.0.0 */public class QMockMvcBuilders {    public static StandaloneMockMvcBuilderWithNoPathExtension standaloneSetup(Object... controllers) {        return new StandaloneMockMvcBuilderWithNoPathExtension(controllers);    }}復制代碼復制代碼
package com.qunar.fresh.exam.web.mockmvc;import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;/** * 一個favorPathExtension=false的StandaloneMockMvcBuilder *  * @author zhenwei.liu created on 2013 13-10-15 上午12:30 * @version 1.0.0 */public class StandaloneMockMvcBuilderWithNoPathExtension extends StandaloneMockMvcBuilder {    /**     * 重設 ContentNegotiationManager, 關閉自動URL后綴檢測     *      * @param controllers 控制器     */    protected StandaloneMockMvcBuilderWithNoPathExtension(Object... controllers) {        super(controllers);        ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();        factory.setFavorPathExtension(false); // 關閉URL后綴檢測        factory.afterPropertiesSet();        setContentNegotiationManager(factory.getObject());    }}復制代碼

另外還有個工具類, 和一個用來創建測試數據的builder

復制代碼
package com.qunar.fresh.exam.web.mockmvc;import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;/** * 一個favorPathExtension=false的StandaloneMockMvcBuilder *  * @author zhenwei.liu created on 2013 13-10-15 上午12:30 * @version 1.0.0 */public class StandaloneMockMvcBuilderWithNoPathExtension extends StandaloneMockMvcBuilder {    /**     * 重設 ContentNegotiationManager, 關閉自動URL后綴檢測     *      * @param controllers 控制器     */    protected StandaloneMockMvcBuilderWithNoPathExtension(Object... controllers) {        super(controllers);        ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();        factory.setFavorPathExtension(false); // 關閉URL后綴檢測        factory.afterPropertiesSet();        setContentNegotiationManager(factory.getObject());    }}復制代碼復制代碼
package com.qunar.fresh.exam.controller.category;import com.qunar.fresh.exam.bean.Category;/** * 用于創建的Category測試數據 * * @author zhenwei.liu created on 2013 13-10-14 下午12:00 * @version 1.0.0 */public class CategoryBuilder {    private int id;    private String name;    public CategoryBuilder id(int id) {        this.id = id;        return this;    }    public CategoryBuilder name(String name) {        this.name = name;        return this;    }    public Category build() {        return new Category(id, name);    }}復制代碼

 

最后看看返回結果

 

復制代碼
MockHttpServletRequest:         HTTP Method = GET         Request URI = /category/manage.htm          Parameters = {}             Headers = {}             Handler:                Type = com.qunar.fresh.exam.controller.CategoryController              Method = public org.springframework.web.servlet.ModelAndView com.qunar.fresh.exam.controller.CategoryController.categoryManage()  Resolved Exception:                Type = null        ModelAndView:           View name = category/categoryList                View = null           Attribute = categoryList               value = [com.qunar.fresh.exam.bean.Category@60e390, com.qunar.fresh.exam.bean.Category@fc40ae]            FlashMap:MockHttpServletResponse:              Status = 200       Error message = null             Headers = {}        Content type = null                Body =        Forwarded URL = category/categoryList      Redirected URL = null             Cookies = []復制代碼

 

 

對表單提交方法進行測試

待提交的bean結構和驗證內容

復制代碼
/** * @author zhenwei.liu created on 2013 13-10-15 下午4:19 * @version 1.0.0 */@WebAppConfiguration@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:spring/service.xml")public class PostControllerTest {    private MockMvc mockMvc;    @Mock    private PostService mockPostService;    @InjectMocks    private PostController postController;    @Before    public void before() {        MockitoAnnotations.initMocks(this);        Mockito.reset(mockPostService);        mockMvc = QMockMvcBuilders.standaloneSetup(postController).build();    }    @Test    public void testPostAddWhenTitleExceeds20() throws Exception {        mockMvc.perform(                post("/post/add").contentType(MediaType.APPLICATION_FORM_URLENCODED)                        .param("title", TestUtil.createStringWithLength(21))                        .param("content", "NaN")).andDo(print())                .andExpect(status().isMovedTemporarily())                .andExpect(redirectedUrl("/post/addPage"))                .andExpect(flash().attributeCount(1))                .andExpect(flash().attribute("errMap", hasKey("title")))                .andExpect(flash().attribute("errMap", hasValue("標題長度必須在2至20個字符之間")));    }}復制代碼

 

Controller方法

復制代碼
import java.util.HashMap;import java.util.Map;import javax.annotation.Resource;import javax.validation.Valid;import org.springframework.stereotype.Controller;import org.springframework.validation.BindingResult;import org.springframework.validation.FieldError;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.mvc.support.RedirectAttributes;import org.springframework.web.servlet.view.RedirectView;import com.qunar.mvcdemo.bean.Post;import com.qunar.mvcdemo.service.PostService;/** * @author zhenwei.liu created on 2013 13-10-12 下午11:51 * @version 1.0.0 */@Controller@RequestMapping("/post")public class PostController {    @Resource    PostService postService;    @RequestMapping("/list")    public ModelAndView list() {        ModelAndView mav = new ModelAndView("post/list");        mav.addObject(postService.fetchPosts());        return mav;    }    @RequestMapping("/addPage")    public ModelAndView addPage(@ModelAttribute HashMap<String, String> errMap) {        return new ModelAndView("post/add");    }    @RequestMapping(value = "/add", method = RequestMethod.POST)    public ModelAndView add(@Valid Post post, BindingResult bindingResult, RedirectAttributes redirectAttributes) {        // 個人認為Spring的錯誤信息局限性太大,不如自己取出來手動處理        if (bindingResult.hasErrors()) {            Map<String, String> errMap = new HashMap<String, String>();            for (FieldError fe : bindingResult.getFieldErrors()) {                errMap.put(fe.getField(), fe.getDefaultMessage());            }            redirectAttributes.addFlashAttribute("errMap", errMap);            return new ModelAndView(new RedirectView("/post/addPage"));        }        postService.addPost(post);        return new ModelAndView(new RedirectView("/post/list"));    }}復制代碼

測試方法

復制代碼
/** * @author zhenwei.liu created on 2013 13-10-15 下午4:19 * @version 1.0.0 */@WebAppConfiguration@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:spring/service.xml")public class PostControllerTest {    private MockMvc mockMvc;    @Mock    private PostService mockPostService;    @InjectMocks    private PostController postController;    @Before    public void before() {        MockitoAnnotations.initMocks(this);        Mockito.reset(mockPostService);        mockMvc = QMockMvcBuilders.standaloneSetup(postController).build();    }    @Test    public void testPostAddWhenTitleExceeds20() throws Exception {        mockMvc.perform(                post("/post/add").contentType(MediaType.APPLICATION_FORM_URLENCODED)                        .param("title", TestUtil.createStringWithLength(21))                        .param("content", "NaN")).andDo(print())                .andExpect(status().isMovedTemporarily())                .andExpect(redirectedUrl("/post/addPage"))                .andExpect(flash().attributeCount(1))                .andExpect(flash().attribute("errMap", hasKey("title")))                .andExpect(flash().attribute("errMap", hasValue("標題長度必須在2至20個字符之間")));    }}復制代碼

注意的點

1. 這個請求鏈使用了 RedirectAttribute的flashAttribute, flashAttribute的是一個基于session的臨時數據, 他使用session暫時存儲, 接收方使用@ModelAttribte 來接受參數使用.

2. 使用了flash().attribute()來判斷錯誤信息是否是期待值

查看輸出

復制代碼
MockHttpServletRequest:         HTTP Method = POST         Request URI = /post/add          Parameters = {title=[274864264523756946214], content=[NaN]}             Headers = {Content-Type=[application/x-www-form-urlencoded]}             Handler:                Type = com.qunar.mvcdemo.controller.PostController              Method = public org.springframework.web.servlet.ModelAndView com.qunar.mvcdemo.controller.PostController.add(com.qunar.mvcdemo.bean.Post,org.springframework.validation.BindingResult,org.springframework.web.servlet.mvc.support.RedirectAttributes)               Async:   Was async started = false        Async result = null  Resolved Exception:                Type = null        ModelAndView:           View name = null                View = org.springframework.web.servlet.view.RedirectView: unnamed; URL [/post/addPage]               Model = null            FlashMap:           Attribute = errMap               value = {title=標題長度必須在2至20個字符之間}MockHttpServletResponse:              Status = 302       Error message = null             Headers = {Location=[/post/addPage]}        Content type = null                Body =        Forwarded URL = null      Redirected URL = /post/addPage             Cookies = []復制代碼

 

對REST API測試

Controller接口

復制代碼
    /**     * 添加分類     *      * @param category     * @return     */    @ResponseBody    @RequestMapping(value = "/add.json", method = RequestMethod.POST)    public Object categoryAdd(@RequestBody @Valid Category category) {        if (!loginCheck()) {            return getRedirectView("/loginPage.htm");        }        // 檢查類目名是否重復        Map<String, Object> params = Maps.newHashMap();        params.put("name", category.getName());        List<Category> test = categoryService.fetchCategories(params);        if (test != null && test.size() != 0) { // 重復類目            return JsonUtils.errorJson("分類名已存在");        }        categoryService.addCategory(category);        logService.addLog(session.getAttribute(USERNAME).toString(), LogType.ADD, "新增課程類目: " + category.getName());        return JsonUtils.dataJson("");    }復制代碼

測試方法

復制代碼
    /**     * 添加已存在課程分類測試 期待返回錯誤信息JSON數據     *      * @throws Exception     */    @Test    @SuppressWarnings("unchecked")    public void testCategoryAddWhenNameDuplicated() throws Exception {        Category duplicatedCategory = new CategoryBuilder().id(1).name(TestUtil.createStringWithLength(5)).build();        String jsonData = new ObjectMapper().writeValueAsString(duplicatedCategory);        when(mockSession.getAttribute(SessionUtil.USERNAME)).thenReturn(TestUtil.createStringWithLength(5));        when(mockCategoryService.fetchCategories(anyMap())).thenReturn(ImmutableList.of(duplicatedCategory));        mockMvc.perform(                post("/category/add.json").contentType(TestUtil.APPLICATION_JSON_UTF8)                        .accept(TestUtil.APPLICATION_JSON_UTF8).content(jsonData)).andDo(print())                .andExpect(status().isOk()).andExpect(content().contentType(TestUtil.APPLICATION_JSON_UTF8))                .andExpect(jsonPath("$.ret", is(false))).andExpect(jsonPath("$.errcode", is(1)))                .andExpect(jsonPath("$.errmsg", is("分類名已存在")));        verify(mockSession, times(1)).getAttribute(SessionUtil.USERNAME);        verifyNoMoreInteractions(mockSession);        verify(mockCategoryService, times(1)).fetchCategories(anyMap());        verifyNoMoreInteractions(mockCategoryService);    }復制代碼

需要注意的是這里需要將請求數據序列化為JSON格式post過去,我們需要設置Accept頭和request content-type以及response content-type

最后是驗證返回的JSON數據是否符合預期要求,這里使用jsonpath來獲取json的特定屬性

輸出如下

復制代碼
MockHttpServletRequest:         HTTP Method = POST         Request URI = /category/add.json          Parameters = {}             Headers = {Content-Type=[application/json;charset=UTF-8], Accept=[application/json;charset=UTF-8]}             Handler:                Type = com.qunar.fresh.exam.controller.CategoryController              Method = public java.lang.Object com.qunar.fresh.exam.controller.CategoryController.categoryAdd(com.qunar.fresh.exam.bean.Category)  Resolved Exception:                Type = null        ModelAndView:           View name = null                View = null               Model = null            FlashMap:MockHttpServletResponse:              Status = 200       Error message = null             Headers = {Content-Type=[application/json;charset=UTF-8]}        Content type = application/json;charset=UTF-8                Body = {"ret":false,"errcode":1,"errmsg":"分類名已存在"}       Forwarded URL = null      Redirected URL = null             Cookies = []復制代碼

 

The End


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 河源市| 古交市| 绥阳县| 德惠市| 贡觉县| 玉林市| 涟水县| 四会市| 卓资县| 东乡| 察隅县| 抚远县| 绵阳市| 潼南县| 赣榆县| 邯郸县| 泰兴市| 军事| 宜良县| 淅川县| 宁陕县| 色达县| 微山县| 弥勒县| 黄浦区| 阳信县| 进贤县| 英德市| 东乡族自治县| 长沙市| 红原县| 蓝山县| 柘荣县| 吉安县| 云和县| 广灵县| 赤城县| 平原县| 理塘县| 扶沟县| 双桥区|