一个测试方法主要包括三部分:

  • setup
  • 执行操作
  • 验证结果
  1. public class CalculatorTest {
  2. Calculator mCalculator;
  3. @Before // setup
  4. public void setup() {
  5. mCalculator = new Calculator();
  6. }
  7. @Test //assert 部分可以帮助我们验证一个结果
  8. public void testAdd() throws Exception {
  9. int sum = mCalculator.add(1, 2);
  10. assertEquals(3, sum); //为了简洁,往往会static import Assert里面的所有方法。
  11. }
  12. @Test
  13. @Ignore("not implemented yet") // 测试时忽略该方法
  14. public void testMultiply() throws Exception {
  15. }
  16. // 表示验证这个测试方法将抛出 IllegalArgumentException 异常,若没抛出,则测试失败
  17. @Test(expected = IllegalArgumentException.class)
  18. public void test() {
  19. mCalculator.divide(4, 0);
  20. }
  21. }

Junit 基本注解介绍

  • @BeforeClass 在所有测试方法执行前执行一次,一般在其中写上整体初始化的代码。
  • @AfterClass 在所有测试方法后执行一次,一般在其中写上销毁和释放资源的代码。
  1. // 注意这两个都是静态方法
  2. @BeforeClass
  3. public static void test(){
  4. }
  5. @AfterClass
  6. public static void test(){
  7. }
  • @Before 在每个方法测试前执行,一般用来初始化方法(比如我们在测试别的方法时,类中与其他测试方法共享的值已经被改变,为了保证测试结果的有效性,我们会在@Before注解的方法中重置数据)
  • @After 在每个测试方法执行后,在方法执行完成后要做的事情。
  • @Test(timeout = 1000) 测试方法执行超过1000毫秒后算超时,测试将失败。
  • @Test(expected = Exception.class) 测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败。
  • @Ignore("not ready yet") 执行测试时将忽略掉此方法,如果用于修饰类,则忽略整个类。
  • @Test 编写一般测试用例用。
  • @RunWith 在 Junit 中有很多个 Runner,他们负责调用你的测试代码,每一个 Runner 都有各自的特殊功能,你根据需要选择不同的 Runner 来运行你的测试代码。

如果我们只是简单的做普通 Java 测试,不涉及 Spring Web 项目,你可以省略 @RunWith 注解,你要根据需要选择不同的 Runner 来运行你的测试代码。

测试方法执行顺序

按照设计,Junit不指定test方法的执行顺序。

  • @FixMethodOrder(MethodSorters.JVM):保留测试方法的执行顺序为JVM返回的顺序。每次测试的执行顺序有可能会所不同。
  • @FixMethodOrder(MethodSorters.NAME_ASCENDING) :根据测试方法的方法名排序,按照词典排序规则(ASC,从小到大,递增)。

Failure 是测试失败,Error 是程序出错。

测试方法命名约定

Maven本身并不是一个单元测试框架,它只是在构建执行到特定生命周期阶段的时候,通过插件来执行JUnit或者TestNG的测试用例。这个插件就是maven-surefire-plugin,也可以称为测试运行器(Test Runner),它能兼容JUnit 3、JUnit 4以及TestNG。

在默认情况下,maven-surefire-plugin的test目标会自动执行测试源码路径(默认为src/test/java/)下所有符合一组命名模式的测试类。这组模式为:

  • */Test*.java:任何子目录下所有命名以Test开关的Java类。
  • */*Test.java:任何子目录下所有命名以Test结尾的Java类。
  • */*TestCase.java:任何子目录下所有命名以TestCase结尾的Java类。

mockito 注解

mockito 定义的注解主要有三个:

  • @Mock
  • @Spy
  • @InjectMocks
    1. // demo
    2. @InjectMocks
    3. private MyService myService;
    4. @Mock
    5. MyMockMapper myMockMapper;
    6. @Spy
    7. MySpyMapper mySpyMapper;
    说明
  1. @Spy和@Mock生成的对象不受spring管理,也不会替换Spring对应类型的bean
  2. @InjectMocks用于自动注入@Spy和@Mock标注的对象

如:MyService 依赖 MyMockMapper、MySpyMapper,则会自动注入对应bean

Mock和Spy异同

@Mock 默认不执行,有返回值的,默认返回null
定义了mock方法的执行mock(即虚假函数);

@Spy 默认会调用真实的方法,有返回值的调用真实方法并返回真实值;
定义了mock方法的执行mock(即虚假函数);

Spring 注解

spring 定义的注解主要有两个:

  • @MockBean
  • @SpyBean

    1. // demo
    2. @Autowired
    3. private MyService myService;
    4. @MockBean
    5. MyMockMapper myMockMapper;
    6. @SpyBean
    7. MySpyMapper mySpyMapper;

    说明

  • @MockBean 和 @SpyBean 生成的对象受spring管理,相当于自动替换对应类型bean的注入

  • 依赖这些bean的上层bean会自动注入 mockBean/spyBean

MyService 依赖 MyMockMapper、MySpyMapper,则会自动注入

  • 如果想指明接口下的哪一个实现,即@Qualifier()功能的话,如下方式:

    1. @MockBean(name = "firstDbJdbcTemplate")
    2. private JdbcTemplate firstDbJdbcTemplate;
    3. @SpyBean(name = "firstDbJdbcTemplate")
    4. private JdbcTemplate firstDbJdbcTemplate;


    基于 Spring 的单元测试编写

首先我们项目一般都是 MVC 分层的,而单元测试主要是在 Dao 层和 Service 层上进行编写。从项目结构上来说,Service 层是依赖 Dao 层的,但是从单元测试角度,对某个 Service 进行单元的时候,他所有依赖的类都应该进行Mock。而 Dao 层单元测试就比较简单了,只依赖数据库中的数据。

引入Mockito

Mockito是mocking框架,它让你用简洁的API做测试。而且Mockito简单易学,它可读性强和验证语法简洁。
Mockito 是一个针对 Java 的单元测试模拟框架,它与 EasyMock 和 jMock 很相似,都是为了简化单元测试过程中测试上下文 ( 或者称之为测试驱动函数以及桩函数 ) 的搭建而开发的工具

相对于 EasyMock 和 jMock,Mockito 的优点是通过在执行后校验哪些函数已经被调用,消除了对期望行为(expectations)的需要。其它的 mocking 库需要在执行前记录期望行为(expectations),而这导致了丑陋的初始化代码。

SpringBoot 中的 pom.xml 文件需要添加的依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>

进入 spring-boot-starter-test-2.1.3.RELEASE.pom 可以看到该依赖中已经有单元测试所需的大部分依赖,如:

  • junit
  • mockito
  • hamcrest

若为其他 spring 项目,需要自己添加 Junit 和 mockito 项目。

常用的 Mockito 方法

方法名 描述
Mockito.mock(classToMock) 模拟对象
Mockito.verify(mock) 验证行为是否发生
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) 触发时第一次返回value1,第n次都返回value2
Mockito.doThrow(toBeThrown).when(mock).[method] 模拟抛出异常。
Mockito.mock(classToMock,defaultAnswer) 使用默认Answer模拟对象
Mockito.when(methodCall).thenReturn(value) 参数匹配
Mockito.doReturn(toBeReturned).when(mock).[method] 参数匹配(直接执行不判断)
Mockito.when(methodCall).thenAnswer(answer)) 预期回调接口生成期望值
Mockito.doAnswer(answer).when(methodCall).[method] 预期回调接口生成期望值(直接执行不判断)
Mockito.spy(Object) 用spy监控真实对象,设置真实对象行为
Mockito.doNothing().when(mock).[method] 不做任何返回
Mockito.doCallRealMethod().when(mock).[method] //等价于Mockito.when(mock.[method] ).thenCallRealMethod()
;
调用真实的方法
reset(mock) 重置mock

@MockBean

使用 @MockBean 可以解决单元测试中的一些依赖问题,示例如下:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class ServiceWithMockBeanTest {
  4. @MockBean
  5. SampleDependencyA dependencyA;
  6. @Autowired
  7. SampleService sampleService;
  8. @Test
  9. public void testDependency() {
  10. when(dependencyA.getExternalValue(anyString())).thenReturn("mock val: A");
  11. assertEquals("mock val: A", sampleService.foo());
  12. }
  13. }

@MockBean 只能 mock 本地的代码——或者说是自己写的代码,对于储存在库中而且又是以 Bean 的形式装配到代码中的类无能为力。

@SpyBean

@SpyBean 解决了 SpringBoot 的单元测试中 @MockBean 不能 mock 库中自动装配的 Bean 的局限。示例如下:

  1. @SpringBootTest(classes = HaloAppStarter.class)
  2. @RunWith(SpringRunner.class)
  3. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
  4. public class ProcPlnmioRecTransServiceImplTest {
  5. @Autowired
  6. private ProcPlnmioRecTransService cProcPlnmioRecTransService;
  7. @SpyBean
  8. private CSetEIQPlnmioRecTransService cSetEIQPlnmioRecTransService;
  9. @SpyBean
  10. private SetAmisQueue cSetAMisQueue;
  11. @SpyBean
  12. private PlnmioRecDao cPlnmioRecDao;
  13. @Before
  14. public void before() {
  15. //指定数据源
  16. String dataSource = "CBPS8_370200_DB";
  17. DynamicDataSourceHolder.set(dataSource);
  18. MockitoAnnotations.initMocks(this);
  19. }
  20. @Test
  21. public void test() {
  22. Mockito.doReturn(1L).when(cPlnmioRecDao).insPlnmioRec(Mockito.any(PlnMioRecPO.class),Mockito.anyString());
  23. Mockito.doNothing().when(cSetEIQPlnmioRecTransService)
  24. .setExernalItfQueue(Mockito.anyString(),Mockito.anyString(),Mockito.any(PlnmioRec.class),Mockito.anyString());
  25. Mockito.doNothing().when(cSetAMisQueue).setAmis(Mockito.any(SetAmisInfoBO.class));
  26. String plnMioRecs ="[{\"mioCustName\":\"打撒的撒\",\"remark\":\"}]";
  27. List<PlnMioRecPO> mulPlnmiorec = JSONObject.parseArray(plnMioRecs,PlnMioRecPO.class);
  28. Assert.assertNotNull(mulPlnmiorec);
  29. InCommonInfo commonInfo = new InCommonInfo();
  30. commonInfo.setAmnt(new BigDecimal("121.80"));
  31. IncommoninfoHolder.set(commonInfo);
  32. cProcPlnmioRecTransService.cfoProcPlnmioRecClaf(mulPlnmiorec);
  33. }
  34. @After
  35. public void after() {
  36. Mockito.reset(cPlnmioRecDao);
  37. Mockito.reset(cSetAMisQueue);
  38. Mockito.reset(cSetEIQPlnmioRecTransService);
  39. }
  40. }

或者使用

  1. MockitoAnnotations.openMocks(this);
  2. ...
  3. Mockito.when(cPlnmioRecDao.insPlnmioRec(Mockito.any(PlnMioRecPO.class),Mockito.anyString()))
  4. .thenReturn(1);

@SpyBean@Spy 的关系类似于 @MockBean@Mock 的关系。和 @MockBean 不同的是,它不会生成一个 Bean 的替代品装配到类中,而是会监听一个真正的 Bean 中某些特定的方法,并在调用这些方法时给出指定的反馈。却不会影响这个 Bean 其它的功能。

注意:使用@SpyBean时候只有Mockito.doxxx方法才会真正改写原本spring注入的bean。

使用示例

  • 验证行为是否发生
  1. //模拟创建一个List对象
  2. List<Integer> mock = Mockito.mock(List.class);
  3. //调用mock对象的方法
  4. mock.add(1);
  5. mock.clear();
  6. //验证方法是否执行
  7. Mockito.verify(mock).add(1);
  8. Mockito.verify(mock).clear();
  • 多次触发返回不同值
  1. //mock一个Iterator类
  2. Iterator iterator = mock(Iterator.class);
  3. //预设当iterator调用next()时第一次返回hello,第n次都返回world
  4. Mockito.when(iterator.next()).thenReturn("hello").thenReturn("world");
  5. //使用mock的对象
  6. String result = iterator.next() + " " + iterator.next() + " " + iterator.next();
  7. //验证结果
  8. Assert.assertEquals("hello world world",result);
  • 模拟抛出异常
  1. @Test(expected = IOException.class)//期望报IO异常
  2. public void when_thenThrow() throws IOException{
  3. OutputStream mock = Mockito.mock(OutputStream.class);
  4. //预设当流关闭时抛出异常
  5. Mockito.doThrow(new IOException()).when(mock).close();
  6. mock.close();
  7. }
  • 使用默认Answer模拟对象

RETURNS_DEEP_STUBS 是创建mock对象时的备选参数之一

以下方法deepstubsTestdeepstubsTest2是等价的

  1. @Test
  2. public void deepstubsTest(){
  3. A a=Mockito.mock(A.class,Mockito.RETURNS_DEEP_STUBS);
  4. Mockito.when(a.getB().getName()).thenReturn("Beijing");
  5. Assert.assertEquals("Beijing",a.getB().getName());
  6. }
  7. @Test
  8. public void deepstubsTest2(){
  9. A a=Mockito.mock(A.class);
  10. B b=Mockito.mock(B.class);
  11. Mockito.when(a.getB()).thenReturn(b);
  12. Mockito.when(b.getName()).thenReturn("Beijing");
  13. Assert.assertEquals("Beijing",a.getB().getName());
  14. }
  15. class A{
  16. private B b;
  17. public B getB(){
  18. return b;
  19. }
  20. public void setB(B b){
  21. this.b=b;
  22. }
  23. }
  24. class B{
  25. private String name;
  26. public String getName(){
  27. return name;
  28. }
  29. public void setName(String name){
  30. this.name = name;
  31. }
  32. public String getSex(Integer sex){
  33. if(sex==1){
  34. return "man";
  35. }else{
  36. return "woman";
  37. }
  38. }
  39. }
  • 参数匹配
  1. @Test
  2. public void with_arguments(){
  3. B b = Mockito.mock(B.class);
  4. //预设根据不同的参数返回不同的结果
  5. Mockito.when(b.getSex(1)).thenReturn("男");
  6. Mockito.when(b.getSex(2)).thenReturn("女");
  7. Assert.assertEquals("男", b.getSex(1));
  8. Assert.assertEquals("女", b.getSex(2));
  9. //对于没有预设的情况会返回默认值
  10. Assert.assertEquals(null, b.getSex(0));
  11. }
  12. class B{
  13. private String name;
  14. public String getName(){
  15. return name;
  16. }
  17. public void setName(String name){
  18. this.name = name;
  19. }
  20. public String getSex(Integer sex){
  21. if(sex==1){
  22. return "man";
  23. }else{
  24. return "woman";
  25. }
  26. }
  27. }
  • 匹配任意参数

Mockito.anyInt() 任何 int 值 ;

Mockito.anyLong() 任何 long 值 ;

Mockito.anyString() 任何 String 值 ;

Mockito.any(XXX.class) 任何 XXX 类型的值 等等。

  1. @Test
  2. public void with_unspecified_arguments(){
  3. List list = Mockito.mock(List.class);
  4. //匹配任意参数
  5. Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
  6. Mockito.when(list.contains(Mockito.argThat(new IsValid()))).thenReturn(true);
  7. Assert.assertEquals(1,list.get(1));
  8. Assert.assertEquals(1,list.get(999));
  9. Assert.assertTrue(list.contains(1));
  10. Assert.assertTrue(!list.contains(3));
  11. }
  12. class IsValid extends ArgumentMatcher<List>{
  13. @Override
  14. public boolean matches(Object obj) {
  15. return obj.equals(1) || obj.equals(2);
  16. }
  17. }

注意:使用了参数匹配,那么所有的参数都必须通过matchers来匹配

Mockito继承Matchers,anyInt()等均为Matchers方法

当传入两个参数,其中一个参数采用任意参数时,指定参数需要matchers来对比

  1. Comparator comparator = mock(Comparator.class);
  2. comparator.compare("nihao","hello");
  3. //如果你使用了参数匹配,那么所有的参数都必须通过matchers来匹配
  4. Mockito.verify(comparator).compare(Mockito.anyString(),Mockito.eq("hello"));
  5. //下面的为无效的参数匹配使用
  6. //verify(comparator).compare(anyString(),"hello");
  • 自定义参数匹配
  1. @Test
  2. public void argumentMatchersTest(){
  3. //创建mock对象
  4. List<String> mock = mock(List.class);
  5. //argThat(Matches<T> matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
  6. Mockito.when(mock.addAll(Mockito.argThat(new IsListofTwoElements()))).thenReturn(true);
  7. Assert.assertTrue(mock.addAll(Arrays.asList("one","two","three")));
  8. }
  9. class IsListofTwoElements extends ArgumentMatcher<List>
  10. {
  11. public boolean matches(Object list)
  12. {
  13. return((List)list).size()==3;
  14. }
  15. }
  • 预期回调接口生成期望值
  1. @Test
  2. public void answerTest(){
  3. List mockList = Mockito.mock(List.class);
  4. //使用方法预期回调接口生成期望值(Answer结构)
  5. Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new CustomAnswer());
  6. Assert.assertEquals("hello world:0",mockList.get(0));
  7. Assert.assertEquals("hello world:999",mockList.get(999));
  8. }
  9. private class CustomAnswer implements Answer<String> {
  10. @Override
  11. public String answer(InvocationOnMock invocation) throws Throwable {
  12. Object[] args = invocation.getArguments();
  13. return "hello world:"+args[0];
  14. }
  15. }
  16. 等价于:(也可使用匿名内部类实现)
  17. @Test
  18. public void answer_with_callback(){
  19. //使用Answer来生成我们我们期望的返回
  20. Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
  21. @Override
  22. public Object answer(InvocationOnMock invocation) throws Throwable {
  23. Object[] args = invocation.getArguments();
  24. return "hello world:"+args[0];
  25. }
  26. });
  27. Assert.assertEquals("hello world:0",mockList.get(0));
  28. Assert. assertEquals("hello world:999",mockList.get(999));
  29. }
  • 预期回调接口生成期望值(直接执行)
  1. @Test
  2. public void testAnswer1(){
  3. List<String> mock = Mockito.mock(List.class);
  4. Mockito.doAnswer(new CustomAnswer()).when(mock).get(Mockito.anyInt());
  5. Assert.assertEquals("大于三", mock.get(4));
  6. Assert.assertEquals("小于三", mock.get(2));
  7. }
  8. public class CustomAnswer implements Answer<String> {
  9. public String answer(InvocationOnMock invocation) throws Throwable {
  10. Object[] args = invocation.getArguments();
  11. Integer num = (Integer)args[0];
  12. if( num>3 ){
  13. return "大于三";
  14. } else {
  15. return "小于三";
  16. }
  17. }
  18. }
  • 修改对未预设的调用返回默认期望(指定返回值)
  1. //mock对象使用Answer来对未预设的调用返回默认期望值
  2. List mock = Mockito.mock(List.class,new Answer() {
  3. @Override
  4. public Object answer(InvocationOnMock invocation) throws Throwable {
  5. return 999;
  6. }
  7. });
  8. //下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值
  9. Assert.assertEquals(999, mock.get(1));
  10. //下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值
  11. Assert.assertEquals(999,mock.size());
  • 用spy监控真实对象,设置真实对象行为
  1. @Test(expected = IndexOutOfBoundsException.class)
  2. public void spy_on_real_objects(){
  3. List list = new LinkedList();
  4. List spy = Mockito.spy(list);
  5. //下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
  6. //Mockito.when(spy.get(0)).thenReturn(3);
  7. //使用doReturn-when可以避免when-thenReturn调用真实对象api
  8. Mockito.doReturn(999).when(spy).get(999);
  9. //预设size()期望值
  10. Mockito.when(spy.size()).thenReturn(100);
  11. //调用真实对象的api
  12. spy.add(1);
  13. spy.add(2);
  14. Assert.assertEquals(100,spy.size());
  15. Assert.assertEquals(1,spy.get(0));
  16. Assert.assertEquals(2,spy.get(1));
  17. Assert.assertEquals(999,spy.get(999));
  18. }
  • 不做任何返回
  1. @Test
  2. public void Test() {
  3. A a = Mockito.mock(A.class);
  4. //void 方法才能调用doNothing()
  5. Mockito.doNothing().when(a).setName(Mockito.anyString());
  6. a.setName("bb");
  7. Assert.assertEquals("bb",a.getName());
  8. }
  9. class A {
  10. private String name;
  11. private void setName(String name){
  12. this.name = name;
  13. }
  14. private String getName(){
  15. return name;
  16. }
  17. }
  • 调用真实的方法
  1. @Test
  2. public void Test() {
  3. A a = Mockito.mock(A.class);
  4. //void 方法才能调用doNothing()
  5. Mockito.when(a.getName()).thenReturn("bb");
  6. Assert.assertEquals("bb",a.getName());
  7. //等价于Mockito.when(a.getName()).thenCallRealMethod();
  8. Mockito.doCallRealMethod().when(a).getName();
  9. Assert.assertEquals("zhangsan",a.getName());
  10. }
  11. class A {
  12. public String getName(){
  13. return "zhangsan";
  14. }
  15. }
  • 重置 mock
  1. @Test
  2. public void reset_mock(){
  3. List list = mock(List.class);
  4. Mockito. when(list.size()).thenReturn(10);
  5. list.add(1);
  6. Assert.assertEquals(10,list.size());
  7. //重置mock,清除所有的互动和预设
  8. Mockito.reset(list);
  9. Assert.assertEquals(0,list.size());
  10. }
  • @Mock 注解
  1. public class MockitoTest {
  2. @Mock
  3. private List mockList;
  4. //必须在基类中添加初始化mock的代码,否则报错mock的对象为NULL
  5. public MockitoTest(){
  6. MockitoAnnotations.initMocks(this);
  7. }
  8. @Test
  9. public void AnnoTest() {
  10. mockList.add(1);
  11. Mockito.verify(mockList).add(1);
  12. }
  13. }
  • 指定测试类使用运行器:MockitoJUnitRunner
  1. @RunWith(MockitoJUnitRunner.class)
  2. public class MockitoTest2 {
  3. @Mock
  4. private List mockList;
  5. @Test
  6. public void shorthand(){
  7. mockList.add(1);
  8. Mockito.verify(mockList).add(1);
  9. }
  10. }

参考

Mockito教程

Unit tests with Mockito - Tutorial