草帽路飞UU 发表于 2022-12-12 15:49:14

使用Mockito进行单元测试分享

本帖最后由 草帽路飞UU 于 2022-12-12 15:50 编辑

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其实还有很多的用法,大家要有兴趣可以自己再找一些资料研究研究。





页: [1]
查看完整版本: 使用Mockito进行单元测试分享