[本文出自天外归云的博客园]
概要简述
利用JUnit结合Mockito,再加上spingframework自带的一些方法,就可以组合起来对Spring MVC中的Controller层进行测试。
在设计测试用例前,我们要对待测Controller的代码逻辑进行逐层深入的走查。走查的目的是要明确Controller中主要逻辑分支,以便设计测试用例进行覆盖。一些主要通用的关注点有:
1. 请求request中所包含的参数值(Controller中从请求中获取的参数)
2. Controller中的try块中能够引起异常的方法调用
3. Controller中的if语句涉及的变量值
4. 一些ThreadLocal方法(在实际测试过程中需要对ThreadLocal对象做一些操作来模拟一些状态)
测试规范
创建后端测试分支:一定是以开发分支为基础创建,也为通过修改开发代码来调试测试代码创造方便。
创建测试类:测试类名B与待测类名A的关系为B=ATest
测试类上添加注释:@RunWith(MockitoJUnitRunner.class)
测试类中声明:private MockMvc mockMvc;
测试类中待注入mock的对象声明上添加注释:@InjectMocks
测试类中待mock的对象声明上添加注释:@Mock
测试方法上添加注释:@Test
常用引用(如果IDE不能自动下载对应maven仓库,则需手动修改pom.xml文件添加引用相应的maven仓库):
import org.junit.Before; import org.junit.Test; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.RunWith; import org.junit.runner.notification.Failure; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
初始化方法标准范例:
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(声明的controller).build(); }
测试方法标准结构范例:
@Test public void testSth() throws Exception { // 自定义填充 // Mock依赖 // 构造请求 // 执行请求与断言 }
对环境ThreadLocal的填充方法举例:
//填充为空 XXThreadLocalContainer.XXX_THREAD_LOCAL.set(null); //填充为指定类型对象 A a= new A(); a.setXX("test"); XXThreadLocalContainer.XXX_THREAD_LOCAL.set(a);
对无返回service方法依赖注入的mock方法举例:
doThrow(Exception.class).when(someService).doSomeMethod(any(SomeClassA.class), any(SomeClassB.class), anyString(), anyString(), any(SomeEnum.class));
对有返回service方法依赖注入的mock方法举例:
// 构造mock方法返回的对象 A a = new A(); a.setSomePropertyA(someValueA); a.setSomePropertyB(someValueB); // 构造mock方法 doReturn(a).when(someService).doSomeMethod(anyString(), anyString());
利用RequestBuilder构造GET请求举例:
RequestBuilder request = MockMvcRequestBuilders.get(someUrl).requestAttr("someAttrNameA", someValueA).requestAttr("someAttrNameB", someValueB).requestAttr("someAttrNameC", someValueC);
执行请求与断言举例:
mockMvc.perform(request).andDo(print()).andExpect(jsonPath("someFieldName").value(String.valueOf(someFieldValue)));
自定义main函数执行测试:
public static void main(String[] args) { Result result = JUnitCore.runClasses(MpResurrectionControllerTest.class); for (Failure failure : result.getFailures()) { System.out.println(String.format("FAILED : %s", failure.toString())); } System.out.println(String.format("TEST SUCCESS : %s", result.wasSuccessful())); }
抽象复用
在实际的测试过程中,把复用的部分提取抽象,生成一个基类为测试类提供继承(MockTestBase.java),其中testAll方法利用反射通过类名动态生成类对象:
package com.xx.xxx; import org.junit.Before; import org.junit.Rule; import org.junit.rules.ExpectedException; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.RunWith; import org.junit.runner.notification.Failure; import org.mockito.MockitoAnnotations; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.test.web.servlet.MockMvc; /** * @Author: Tylan * @CreateDate: 2018/7/2 11:07 * @UpdateDate: 2018/7/2 11:07 */ @RunWith(MockitoJUnitRunner.class) public abstract class MockTestBase { MockMvc mockMvc; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { MockitoAnnotations.initMocks(this); } public static void testAll(String className) throws ClassNotFoundException { Class obj = Class.forName(className); Result result = JUnitCore.runClasses(obj); for (Failure failure : result.getFailures()) { System.out.println(String.format("FAILED : %s", failure.toString())); } System.out.println(String.format("TEST SUCCESS : %s", result.wasSuccessful())); } }
新的测试类就变成这样,重新写一个main函数调用基类testAll方法,利用反射传入当前运行的类名:
package com.xx.xxx; import com.xx.xxx.ForTestController; import com.xx.xxx.SomeService; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; /** * @Author: Tylan * @CreateDate: 2018/7/2 11:11 * @UpdateDate: 2018/7/2 11:11 */ public class NewMpGameOrderControllerTestsTylan extends MockTestBase { @InjectMocks private ForTestController someController; @Mock SomeService someService; @Override public void setUp() { this.mockMvc = MockMvcBuilders.standaloneSetup(someController).build(); } @Test public void insertTestWrongParam() throws Exception { /* * 测试data为空 * */ System.out.println("insertTestWrongParam"); // 构造测试数据 String data1 = ""; String data2 = ""; String insertUrl = "/xx/xx/data/insert"; // 执行请求与断言 RequestBuilder request = MockMvcRequestBuilders.get(insertUrl).requestAttr("data1", data1).requestAttr("data2", data2); mockMvc.perform(request).andDo(print()).andExpect(jsonPath("xxx").value("xx")); } public static void main(String[] args) throws ClassNotFoundException { testAll(Thread.currentThread().getStackTrace()[1].getClassName()); } }
这就生成了一个测试Controller的模板。剩下的工作就是分析开发源码,设计测试用例并对Controller中的所有逻辑分支进行覆盖测试了。
在一个单元测试用例,标准顺序是mock、test、verify三个环节。其中mock举例:
GameOrder order = new GameOrder(); order.setId(xxx); order.setScore(xxx); doReturn(order).when(gameService).initGameOrder(anyString(), anyString(), anyString(), any(MpEnum.class));
涉及对foreach循环的mock举例:
IteratormockIter = mock(Iterator.class); when(mockIter.hasNext()).thenReturn(true, true, true, false); when(mockIter.next()).thenReturn(someService).thenReturn(someService).thenReturn(someService); when(serviceList.iterator()).thenReturn(mockIter); doReturn(SomeEnum.XXXXX).when(someService).getSomeEnum(); doReturn(true).when(someService).isValid(anyMap());
对于test环节,测试Controller就是模拟给Controller发请求,测试Service就是直接调用Service方法。
对于Verify环节,主要是对一些方法是否执行,路径是否走过做一下验证,以及一些值的验证。这里举个例子:
//Verify verify(someService).initGameOrder(xx, xxx, xxxx, someEnum); verify(someService).update(any(GameOrder.class));
这里验证了someService对象是否执行了initGameOrder方法和update方法。
感受与总结
想做好单元测试,要对待测试代码逻辑进行充分分析,重点逻辑是在Service层和Controller层的测试,而对于这两层来说,Controller中除了一些基本逻辑基本就是service层的调用,对于service层,除了定义service的interface类就是定义对应implements的接口实现类,interface类只定义方法接口,剩余的由service的impl类实现,service实现类中除了Override实现继承的接口类定义的方法接口就是Autowired声明一些在该实现类中要用到的其他service接口类(非实现类),剩下的就是接口类一层又一层的继承关系,而service层无非是围绕着dao层展开的,最终落在了dao层对数据库或缓存的操作上。所以纵观整个后端结构顺序就是Filter层-Controller层-Service层-Dao层,请求是按这个顺序前进与原路返回的。所以后端测试的重点,最终是落在Controller层逻辑的正确性与sql查询、redis和memcache等缓存查询的正确性上。
单测是否应该由开发人员进行?这个问题的答案和开发是否应该由测试人员完成是一样的,如果让开发搞测试没什么不可以,那么让测试做开发也能更好的保障质量。但是现实的分工中,没有那么多全栈人才,测试人员不懂开发,开发人员不懂测试。单测是应该由测试人员完成的,想要保障产品质量,如果QA都对代码逻辑没有了解的话,单凭瞎子摸象这种经验推测式的方法进行测试,能够发现的问题种类和层次也是有限的。而开发除了紧张的开发周期外,还要修改bug,没有更多的时间来投入到测试中。所以测试人员应该具备能够发现bug和定位问题原因的能力才能够更好的配合开发一起完成高质量的产品软件工程。