Yuandupier

Yuandupier

使用Mockito进行单元测试实践

17
0
0
2022-01-27

Mockito简介以及工作流程

Mockito是一个用于在软件测试中模拟对象的开源框架,使用Mockito很大程度简化了对具有外部依赖项的类的测试开发。

mock的对象就是接口或者类的一个虚拟的实现,他允许自己定义方法的输出。通常是模拟比如和其他系统的交互信息然后再进行测试验证。

mock的流程:

在这里插入图片描述

  1. mock出来测试类的依赖,自定义输出的结果
  2. 执行测试类代码
  3. 验证执行的结果是否和预期一致

工程添加Mockito依赖

项目使用的是maven构建,需要添加下面的依赖。我项目中使用的是Junit4。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>3.3.3</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

Mockito的使用

如何mock出一个新的对象

Mockito提供一下几种方式来创建出mock的对象:

  • 在Junit5中,使用@ExtendWith(MockitoExtension.class)注解和@Mock注解结合使用来mock对象
  • 使用静态的mock()方法直接mock对象
  • 使用@Mock注解,这边注意如果要使用@Mock注解的话,需要触发一个初始化的操作,初始化方法是一个静态的方:MockitoAnnotations.initMocks(this)

下面看一个简单的例子:

新建两个类,Database和Service类,Database中提供两个方法,isAvailable和getUniqueId,Service类中依赖Database,提供一个query方法调用databse的isAvailable方法,都只是测试使用。

/**
 * 提供两个方法
 *
 * @since 2022/1/4
 */
public class Database {
    public boolean isAvailable() {
        return false;
    }

    public int getUniqueId() {
        return 22;
    }
}

/**
 * 依赖database
 *
 * @since 2022/1/4
 */
public class Service {
    private final Database database;

    public Service(Database database) {
        this.database = database;
    }

    public boolean query(String query) {
        return database.isAvailable();
    }

    @Override
    public String toString() {
        return "Using database with id: " + database.getUniqueId();
    }
}

使用Mockito来模拟Database对象的单元测试可以按照下面的方式编写:

/**
 * @since 2022/1/4
 */
public class ServiceTest {
    @Mock
    Database database; // 2

    @InjectMocks
    Service service; // 3

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this); // 1
    }

    @Test
    public void testQuery() {
        Assert.assertNotNull(database);
        Mockito.when(database.isAvailable()).thenReturn(true); // 4
        final boolean query = service.query("select * from database"); // 5
        Assert.assertTrue(query); 
    }
}
  1. 第一步先触发一个初始化的操作,初始化方法是一个静态的方:MockitoAnnotations.initMocks(this),写在了Before方法下面。
  2. 在第一步的基础之上,可以使用@Mock注解mock出来一个database对象。
  3. @InjectMocks注解表示可以通过构造函数、setter方法或者属性注入的方式注入mock的对象,这边service对象提供了一个包含database的构造函数,就可以通过这个注解把mock的database注入进来。
  4. 配置mock的对象,设置当isAvailable方法被调用的时候返回true,这些后面会再说明一下。
  5. 执行测试的方法,然后用断言判断方法的返回值是否正确。

在mock的对象上面配置方法调用的返回值

上面讲过,Mockito可以自己配置在mock对象上调用方法的返回值。同时,对于没有配置返回值的方法,会返回空值:

  • 对于Object类型返回null
  • 数字类型返回0
  • boolean类型返回false
  • ......

下面说明一下几个常用的用法:

  1. when().thenReturn() 和 when().thenThrow()

    在这里插入图片描述

    使用when(...).thenReturn(...)可以在方法调用的时候,使用预置的参数来返回想要指定的值。

    也可以使用比如anyString或者anyInt这些方法来定义输入的参数,这边就是对于所有的输出都返回指定的值,注意,在mock的时候要么全部使用anyString这种模拟的参数,要么就全部使用真实的参数,不然mock会失败。

    如果想定义多个返回的值,thenReturn也可以支持链式的调用,他会按照指定的顺序一个一个的返回。

    具体案例:

    /**
     * @since 2022/1/21
     */
    public class MockitoWhenTest {
        @Mock
        List<String> mockList;
    
        @Mock
        Comparator<Integer> comparator;
    
        @Before
        public void setUp() {
            MockitoAnnotations.initMocks(this);
        }
    
        // 测试返回指定的值 指定参数
        @Test
        public void testReturnConfiguredValue() {
            Mockito.when(mockList.get(0)).thenReturn("Hello");
            Assert.assertEquals(mockList.get(0), "Hello");
        }
    
        // mock可以指定多个返回值 会按照顺序返回
        @Test
        public void testMoreThanOneReturnValue() {
            Mockito.when(mockList.get(0)).thenReturn("Hello").thenReturn("World");
            Assert.assertEquals(mockList.get(0), "Hello");
            Assert.assertEquals(mockList.get(0), "World");
        }
    
        // 可以指定anyInt anyString等类型 不限定参数的输入 都返回配置的值
        @Test
        public void testReturnValueUseAnyParameter() {
            // 测试用 不要在意功能
            Mockito.when(comparator.compare(9999,9999)).thenReturn(100);
            Assert.assertEquals(comparator.compare(9999,9999), 100);
    
            Mockito.when(comparator.compare(anyInt(), anyInt())).thenReturn(222);
            Assert.assertEquals(comparator.compare(9,232), 222);
        }
    }
    

    使用when().thenReturn()可以用来抛出异常,使用方法很简单,具体案例:

    /**
     * @since 2022/1/21
     */
    public class MockitoThrowTest {
    
        @Test
        public void testThrow() {
            // 这边使用静态的mock方法模拟对象
            Demo demo = Mockito.mock(Demo.class);
            Mockito.when(demo.getAge(anyInt())).thenThrow(new IllegalArgumentException("Warning"));
            IllegalArgumentException exception = Assert.assertThrows(IllegalArgumentException.class, () -> demo.getAge(33));
            Assert.assertEquals(exception.getMessage(), "Warning");
        }
        
        static class Demo {
            public int getAge(int age) {
                return age;
            }
        }
    }
    
  2. doReturn().when() 和doThrow().when()

    这两个方法和when().thenReturn()、when().thenThrow()方法一样,都可以模拟mock对象方法的返回,后面这种应该好理解一点。但是对使用@Spy注解模拟的对象来说,doReturn这种方法是有用的。

    对于真实的对象,可以通过@Spy注解来模拟。而且真实的对象的话,会调用真实的方法,如果继续用when().thenReturn()的话,会产生副作用,所以对于这种真实的对象,需要使用doReturn来打桩。emmm,这边我也是根据javadoc翻译的,具体的例子:

    /**
     * @since 2022/1/21
     */
    public class MockitoDoReturnTest {
        @Spy
        List<String> spyList = new LinkedList<>();
    
        @Mock
        List<String> mockList;
    
        @Before
        public void setUp() {
            MockitoAnnotations.initMocks(this);
        }
    
        @Test
        public void doReturnTest() {
            // Mockito.when(spyList.get(0)).thenReturn("Hello");
            // Assert.assertEquals(spyList.get(0), "Hello");
            // 这边会抛出java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 异常
            // spyList其实是一个空列表
            Mockito.doReturn("Hello").when(spyList).get(0);
            Assert.assertEquals(spyList.get(0), "Hello");
    
            Mockito.doReturn("World").when(mockList).get(1);
            Assert.assertEquals(mockList.get(1), "World");
        }
    }
    

Mockito中的行为测试

使用verify()方法可以验证mock的方法是否满足指定的条件,比如可以验证传入的是否是自己指定的参数,该方法调用了几次等等。这种测试被称作行为测试,行为测试不会影响方法调用的接口,但是会检查是否使用了正确的参数调用方法。

示例:

/**
 * @since 2022/1/21
 */
public class MockitoVerifyTest {
    @Mock
    private Dog dog;
    
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testVerify() {
        dog.setAge(22);
        dog.setName("Test1");
        dog.setName("Test2");

        // 验证是否设置了age为22
        Mockito.verify(dog).setAge(ArgumentMatchers.eq(22));
        // 验证是否调用了2次setName("Test1")方法
        Mockito.verify(dog, Mockito.times(1)).setName("Test1");

        // 验证是否重来没有调用过指定的方法
        Mockito.verify(dog, Mockito.never()).getAge();
        Mockito.verify(dog, Mockito.never()).setName("Test3");
        // 验证最后一次的mock方法是否是setName("Test2")
        Mockito.verify(dog, Mockito.atLeast(1)).setName("Test2");
    }

    @Data
    static class Dog {
        private int age;
        private String name;
    }
}

使用@InjectMocks进行依赖注入

之前第一个例子说明过@InjectMocks注解的用法,Mockito可以通过构造函数,setter方法或者属性根据类型来注入mock的对象,下面再提供一个示例。

代码示例:

/**
 * @since 2022/1/21
 */
@Setter
public class AccountService {
    // 依赖accountDao以及personDao对象
    private AccountDao accountDao;
    private PersonDao personDao;

    public double getBalance() {
        return accountDao.getBalance();
    }

    public String getName() {
        return personDao.getName();
    }
}

/**
 * @since 2022/1/21
 */
public class AccountDao {
    public double getBalance() {
        return 0;
    }
}

/**
 * @since 2022/1/21
 */
public class PersonDao {
    public String getName() {
        return "";
    }
}
/**
 * @since 2022/1/21
 */
public class MockitoInjectTest {

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Mock
    private AccountDao accountDao;

    @Mock
    private PersonDao personDao;

    @InjectMocks
    private AccountService accountService;

    @Test
    public void testInjectMocks() {
        // mock出accountDao以及personDao对象
        Mockito.when(accountDao.getBalance()).thenReturn(55555.0);
        Mockito.when(personDao.getName()).thenReturn("Jack");

        Assert.assertEquals(accountService.getBalance(), 55555.0, 0);
        Assert.assertEquals(accountService.getName(), "Jack");
    }
}		

对复杂的mock使用Answers

上面讲到了thenReturn,doReturn都是对mock的方法直接指定了返回的值,对于一些复杂的mock的场景,比如说我们想根据指定的参数来计算出返回值,或者对参数做一个回调,可以使用Answers方法。

具体案例:

/**
 * @since 2022/1/22
 */
public class MockitoAnswerTest {

    @Test
    public void doAnswerTest() {
        final Car car = Mockito.mock(Car.class);
        Mockito.doAnswer(invocation -> {
            // 根据参数计算返回值
            final String color = invocation.getArgument(0, String.class);
            final String brand = invocation.getArgument(1, String.class);
            return color + " " + brand;
        }).when(car).getName("red", "BMW");
        Assert.assertEquals(car.getName("red", "BMW"), "red BMW");
    }

    static class Car {
        public String getName(String color, String brand) {
            return "";
        }
    }
}

结语

关于Mockito的使用就总结到这边,Mockito其实还有很多的用法,大家要有兴趣可以自己再找一些资料研究研究。

参考连接:https://www.vogella.com/tutorials/Mockito/article.html

代码地址:https://github.com/yzh19961031/blogDemo/tree/master/mockitoTest