使用Mockito进行单元测试实践

发表于:2022-12-08 09:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:洒脱的智障    来源:CSDN

  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其实还有很多的用法,大家要有兴趣可以自己再找一些资料研究研究。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号