(6). 验证执行执行顺序
// A. 验证mock一个对象的函数执行顺序
// 创建Mock对象
List singleMock = mock(List.class);
// 使用mock对象
singleMock.add("was added first");
singleMock.add("was added second");
// 为该mock对象创建一个inOrder对象
InOrder inOrder = inOrder(singleMock);
// 确保add函数首先执行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B .验证多个mock对象的函数执行顺序
List firstMock = mock(List.class);
List secondMock = mock(List.class);
// 使用mock对象
firstMock.add("was called first");
secondMock.add("was called second");
// 为这两个Mock对象创建inOrder对象
InOrder inOrder = inOrder(firstMock, secondMock);
// 验证它们的执行顺序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
验证执行顺序是非常灵活的。你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可。另外,你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。
(7). 确保交互(interaction)操作不会执行在mock对象上
// 使用Mock对象
mockOne.add("one");
// 普通验证
verify(mockOne).add("one");
// 验证某个交互是否从未被执行
verify(mockOne, never()).add("two");
// 验证mock对象没有交互过
verifyZeroInteractions(mockTwo, mockThree);
(8). 查找冗余的调用
// 使用mock对象
mockedList.add("one");
mockedList.add("two");
verify(mockedList).add("one");
// 下面的验证将会失败
verifyNoMoreInteractions(mockedList);
一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用。verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时。滥用它将导致测试代码的可维护性降低。你可以阅读这篇文档来了解更多相关信息。
(9). 简化mock对象的创建
最小化重复的创建代码;
使测试类的代码可读性更高;
使验证错误更易于阅读,因为字段名可用于标识mock对象;
public class ArticleManagerTest {
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;
注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:
MockitoAnnotations.initMocks(testClass);
关于mock注解的更多信息可以阅读MockitoAnnotations文档。
(10). 为连续的调用做测试打桩 (stub)
有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
// 第一次调用 : 抛出运行时异常
mock.someMethod("some arg");
// 第二次调用 : 输出"foo"
System.out.println(mock.someMethod("some arg"));
// 后续调用 : 也是输出"foo"
System.out.println(mock.someMethod("some arg"));
另外,连续调用的另一种更简短的版本 :
// 第一次调用时返回"one",第二次返回"two",第三次返回"three"
when(mock.someMethod("some arg"))
.thenReturn("one", "two", "three");
(11). 为回调做测试桩
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});
// 输出 : "called with arguments: foo"
System.out.println(mock.someMethod("foo"));
(12). 监控真实对象
你可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码。
List list = new LinkedList();
List spy = spy(list);
// 你可以为某些函数打桩
when(spy.size()).thenReturn(100);
// 通过spy对象调用真实对象的函数
spy.add("one");
spy.add("two");
// 输出第一个元素
System.out.println(spy.get(0));
// 因为size()函数被打桩了,因此这里返回的是100
System.out.println(spy.size());
// 交互验证
verify(spy).add("one");
verify(spy).add("two");
Mockito 并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。
因此结论就是: 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。
(13). 重置mocks对象
聪明的 Mockito 使用者很少会用到这个特性,因为他们知道这是出现糟糕测试单元的信号。通常情况下你不会需要重设你的测试单元,只需要为每一个测试方法重新创建一个测试单元就可以了。
如果你真的想通过reset()方法满足某些需求的话,请考虑实现简单,小而且专注于测试方法而不是冗长,精确的测试。首先可能出现的代码异味就是测试方法中间那的reset()方法。这可能意味着你已经过度测试了。
添加 reset() 方法的唯一原因就是让它能与容器注入的测试单元协作。
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
//at this point the mock forgot any interactions & stubbing
(14). 更多的注解
@Captor: 创建ArgumentCaptor。
@Spy: 可以代替spy(Object)。
@InjectMocks: 如果此注解声明的变量需要用到mock对象,mockito会自动注入mock或spy成员。
//可以这样写
@Spy
BeerDrinker drinker = new BeerDrinker();
//也可以这样写,mockito会自动实例化drinker.
@Spy
BeerDrinker drinker;
//会自动实例化LocalPub
@InjectMocks
LocalPub pub;
(15). BDD 风格的验证(Since 1.10.0)
开启Behavior Driven Development(BDD,即行为驱动开发)风格的验证可以通过BBD的关键词then开始验证。
given(dog.bark()).willReturn(2);
// when
...
then(person).should(times(2)).ride(bike);
以上就是 Mockito 的主要使用方式,关于更详细的介绍可参考Mockito官方文档和Mockito中文文档。
4. Spring Test
目前几乎大多数 Java web 项目都是有基于 Spring 来开发的。通过 Spring 进行 bean 管理后,仅仅通过 JUnit 来做测试会有各种麻烦,比如:Spring容器初始化问题、使用硬编码方式手工获取Bean、不方便对数据操作的正确性做检查等。这时我们就可以通过 Spring 全家桶中的另一位成员spring-test来帮助我们在 Spring 工程中做单元测试了。以下通过简单的示例来演示其使用。
(1). 加入依赖包
通过Maven加入JUnit、spring-test的Jar包(最好其他Spring包版本一致)。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>xxxx</version> <scope>test</scope> </dependency> |
(2). 创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("/application-context-test.xml") public class UserDaoTest { /** 自动注入baseDao,默认按名称. */ @Resource private IBaseDao baseDao; @Test @Transactional @Rollback public void insert() { String sql = "INSERT INTO t_user(c_name, c_password) values(?, ?)"; Object[] objs = new Object[]{"zhangsan", "123456"}; baseDao.insert(sql , objs); String sql2 = "SELECT * FROM t_user WHERE c_name = ? and c_password = ?"; List<Map<String,Object>> list = baseDao.queryForList(sql1, objs); assertTrue(list.size() > 0); System.out.println(list); } } |
使用Spring Test 可以使用@Autowired自动注入相关的bean信息,而不需要自己手动通过getBean去获取相应的bean信息。
使用Spring Test 测试,可以@Transaction注解,表示该方法使用spring的事务,在单元测试中,执行完毕后默认会回滚。
使用@Rollback注解,标明使用完此方法后事务回滚,可以@Rollback(false)这个注解来使对数据库操作的测试结果不回滚。
(3). 对 Spring MVC 的测试
为了测试 web 项目,需要一些 Servlet 相关的模拟对象,比如:MockMVC/MockHttpServletRequest/MockHttpServletResponse/MockHttpSession。使用示例如下:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("/application-context-test.xml") @WebAppConfiguration("src/main/resources") // 此注解指定web资源的位置,默认为src/main/webapp public class TestControllerIntegrationTests { private MockMvc mockMvc; // 模拟MVC对象 @Autowired private DemoService demoService;// 在测试用例注入spring的bean @Autowired WebApplicationContext wac; // 注入WebApplicationContext @Autowired MockHttpSession session; // 注入模拟的http session @Autowired MockHttpServletRequest request; // 模拟request @Before // 测试开始前的初始化工作 public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //2 } @Test public void testNormalController() throws Exception{ String exp_str = demoService.saySomething(); // expect str mockMvc.perform(get("/normal")) // 模拟GET /normal .andExpect(status().isOk())// 预期返回状态为200 .andExpect(view().name("page"))// 预期view的名称 .andExpect(forwardedUrl("/WEB-INF/classes/views/page.jsp"))// 预期页面转向的真正路径 .andExpect(model().attribute("msg", exp_str));// 预期model里的值 } @Test public void testRestController() throws Exception{ mockMvc.perform(get("/testRest")) // HTTP GET 方法 .andExpect(status().isOk()) .andExpect(content().contentType("text/plain;charset=UTF-8"))//14 .andExpect(content().string(demoService.saySomething()));//15 } } |
注: demoService及相关方法的调用,也可以通过Mockito工具Mock出来,更符合单元测试对单元性的要求,否则这些测试又额外附带了一定集成测试的性质了。
4. spring-boot-starter-test
(1). 简单介绍
现在越来越多的应用都采用SpringBoot的方式来构建,在SpringBoot应用中单元测试变得更加容易了,只需要加入spring-boot-starter-test的 Starter 即可,其中默认导入了 Spring Boot 测试模块以及JUnit,AssertJ,Hamcrest和其他一些有用的库。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> |
spring-boot-starter-test的 Starter (Scope为test),包括了以下提供的类库:
JUnit:单元测试Java应用程序的事实标准。
Spring Test 和 Spring Boot Test:Spring Boot应用程序的实用程序和集成测试支持。
AssertJ:流畅的断言库。
Hamcrest:匹配器对象库。
Mockito:Java Mock 框架。
JSONassert:JSON的断言库。
JsonPath:JSON的XPath。
我们通常在编写测试时发现这些通用库都是比较有用的。如果这些库还不适合您的需求,您还可以添加您自己的附加测试依赖库。
Spring Boot 提供了一个@SpringBootTest注释,当您需要 Spring Boot 功能时,它可以用作标准 spring-test @ContextConfiguration注释的替代方法。注解的工作原理是通过SpringApplication创建用于测试的ApplicationContext。除了@SpringBootTest之外,还提供了许多其他注释来测试应用程序的更具体的切片。
提示:不要忘记在测试中添加@RunWith(SpringRunner.class),否则注释将被忽略。
(2). 一个简单示例
@RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTest { @Value("${msg}") private String msg; @Autowired private UserService userService; @Test public void getUser() { User user = userService.selectByKey(20180302325L); Assert.assertThat(user.getName(), is("Blinkfox")); System.out.println("获取的配置信息为:" + msg); } } |
上面就是最简单的单元测试写法,测试类上只需要@RunWith(SpringRunner.class)和@SpringBootTest两个注解即可测试任何类和方法。
(3). web模块的单元测试
要测试 Spring MVC 控制器是否按预期工作,请使用@WebMvcTest注释。@WebMvcTest自动配置Spring MVC基础结构,并将扫描的bean限制为@Controller,@ControllerAdvice,@JsonComponent,Converter,GenericConverter,Filter,WebMvcConfigurer和HandlerMethodArgumentResolver。 使用此注释时,不会扫描常规的@Component bean。
您还可以使用@AutoConfigureMockMvc对其进行注释,从而在非@WebMvcTest(如@SpringBootTest)中自动配置MockMvc。 以下示例使用MockMvc:
@RunWith(SpringRunner.class) @WebMvcTest(UserVehicleController.class) public class MyControllerTests { @Autowired private MockMvc mvc; @MockBean private UserVehicleService userVehicleService; @Test public void testExample() throws Exception { given(this.userVehicleService.getVehicleDetails("sboot")) .willReturn(new VehicleDetails("Honda", "Civic")); this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()).andExpect(content().string("Honda Civic")); } } |
SpringBoot对各种单元测试的场景支持的比较全,更多的示例可直接在Spiring Boot Test 官方指南中去查看,这里就不再一一列举了。
5. JaCoCo
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。 目前Java常用覆盖率工具clover、Jacoco和Cobertura等。关于这些代码覆盖率工具的对比可参看这里。这里我们就选取 Jacoco 来作为代码覆盖率工具来做介绍。
Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到Ant 、Maven中,并提供了 Eclipse、IDEA 插件,也可以使用Java Agent技术监控Java程序。很多第三方的工具提供了对 Jacoco 的集成,如sonar、Jenkins。
Jacoco与Maven的集成很简单,只需要在plugins中添加如下插件即可。
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.7.7.201606060606</version> <configuration> <destFile>target/coverage-reports/jacoco-unit.exec</destFile> <dataFile>target/coverage-reports/jacoco-unit.exec</dataFile> </configuration> <executions> <execution> <id>jacoco-initialize</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>jacoco-site</id> <phase>package</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> |
做单元测试时,测试覆盖率是不是越高代表代码质量越好呢?Martin Fowler(重构那本书的作者)曾经写过一篇博客来讨论这个问题,他指出:把测试覆盖作为质量目标没有任何意义,而我们应该把它作为一种发现未被测试覆盖的代码的手段。
所以,代码覆盖率统计是用来发现没有被测试覆盖的代码;代码覆盖率统计不能完全用来衡量代码质量。