Java单元测试

news2024/9/20 1:24:40

1. 序言

1.1 工作中要求进行单元测试

  • 毕业进入公司时,为了锻炼笔者的Java基础,老大给笔者分配了平台化开发的工作,基于Spring Boot + Mybatis的Java Web后端开发
  • 一个人干后端开发,且以前也没有后端开发的经验,所以只是简单地模仿前人的代码,几乎没有使用任何的设计模式或者常见的Spring技术
  • 反正一句话:用最简单的语法,编写最冗长的代码,仅满足当时需求,且不考虑后续扩展
  • 在开发初期,组长就提出小组开发规范:开发人员要编写单元测试,以保证代码开发质量
  • 长期摸索后,自己的单元测试代码写得叫一个 “溜”
  • 最近学习了一些单元测试的知识后,回想自己的单元测试代码,就是一坨💩 😂

1.2 说说自己的单元测试代码

  • 以一个判断用户是否为系统管理员的service方法为例:

    // 对应的interface
    public interface AuthorityService {
    	boolean isSystemAdministrator(String user);
    }
    
    // 对应的service实现
    @Service
    public class AuthorityServiceImpl implements AuthorityService {
        private final static Logger logger = LoggerFactory.getLogger(AuthorityServiceImpl.class);
        @Autowired
        private AuthorityMapper authorityMapper;
       
        @Override
        public boolean isSystemAdministrator(String user) {
            // 系统管理员的定义:属于system项目,且角色为SYSTEM
        	Authority authority = authorityMapper.getByUserAndProject(user, "system");
            if (authority != null && Role.SYSTEM.equals(authority.getRole())) {
                return true;
            }
            return false;
        }
    }
    
  • MySQL数据库的访问,是使用Mapper接口 + Mapper.xml实现的

    // 对应的Mapper.xml实现,这里省略
    public interface AuthorityMapper extends BaseMapper<Authority> {
        Authority getByUserAndProject(String user, String project);
    }
    
  • 单元测试的逻辑(这里暂不考虑代码执行存在异常的情况):

    • system资源组中,不存在该用户,直接返回false
    • system资源组中,存在用户且角色为SYSTEM,返回true;否则,返回false
  • 整体的单元测试代码如下:

    // 这里一系列的注解,都是为了能在单元测试时启动整个服务,比如连接数据库、访问配置中心等
    // 这里主要是为了实现数据库的连接
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = PlatformApplication.class)
    @DirtiesContext
    public class AuthorityServiceImplTest {
        private final static Logger logger = LoggerFactory.getLogger(AuthorityServiceImplTest.class);
      
        @Rule
        public MockitoRule rule = MockitoJUnit.rule();
    
        @Autowired
        AuthorityService authorityService;
      
        @Test
        @Transactional // 单元测试结束后,会清理数据库中的记录
        public void testIsSystemAdministrator() {
        	// 插入一条权限记录
            String user = RandomStringUtils.random(8, false, true);
            Authority authority = new Authority(user, Role.SYSTEM, "11120066", "system_test", "2020-10-26 12:24:45", true);
            authorityService.add(authority);
            // 资源组不是system,因此直接返回false
            Assert.assertFalse(authorityService.isSystemAdministrator(user));
            
            // 插入一条权限记录
            user = RandomStringUtils.random(8, false, true);
            authority = new Authority(user, Role.SYSTEM, "11120066", "system", "2020-10-26 12:24:45", true);
            authorityService.add(authority);
            // 项目为system且角色为SYSTEM,是系统管管理员,返回true
            Assert.assertTrue(authorityService.isSystemAdministrator(user));
    
            // 插入一条权限记录
            user = RandomStringUtils.random(8, false, true);
            authority = new Authority(user, Role.NORMAL, "11120066", "system", "2020-10-26 12:24:45", true);
            authorityService.add(authority);
            // 项目为system,但角色不是SYSTEM,不是系统管理员,返回false
            Assert.assertFalse(authorityService.isSystemAdministrator(user));
        }
    }
    
  • 主要使用到了如下的jar包依赖:

    <dependency>
    	<groupId>junit</groupId>
    	<artifactId>junit</artifactId>
    	<version>4.12</version>
    </dependency>
    
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>4.3.22.RELEASE</version>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-test</artifactId>
    	<version>1.5.19.RELEASE</version>
    	<scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.3.22.RELEASE</version>
        <scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>4.3.22.RELEASE</version>
    </dependency>
    

1.3 存在的问题

  • 从单元测试的代码可以看出,service方法依赖DAO层,也就是需要访问数据库
  • 为了能在单元测试时连接数据库,笔者使用了AuthorityServiceImplTest类开头的三个注解,使得单元测试时能启动整个应用
  • 同时,笔者在测试时以代码的形式自动向数据库插入权限记录,并使用@Transactional注解在单元测试执行完后清理数据
  • 这样可以避免手动建造数据或者后期数据发生变化,导致单元测试无法执行的情况
  • 就算是这样的单元测试,也存在一些问题,引用其他博客的描述就是:
    1. This unit test is slow, because you need to start a database in order to get data from DAO.
    2. This unit test is not isolated, it always depends on external resources like database.
    3. This unit test can’t ensures the test condition is always the same, the data in the database may vary in time.
    4. It’s too much work to test a simple method, cause developers skipping the test.
  • 总之一句话,我们希望单元测试是简单、快速且可靠的,而非像现在这样负载、缓慢且不稳定

    In summary, what we want is a simple, fast, and reliable unit test instead of a potentially complex, slow, and flaky test!

1.4 正确的单元测试

  • 使用Mockit实现DAO层接口的模拟,使其无需访问数据库,就能在在特定入参条件下,返回特定的值
  • 这里只展示测试逻辑中的一种,用户是系统管理员的情况
    import org.junit.Assert;
    import org.junit.Test;
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
    
    public class AuthorityServiceTest {
        @Test
        public void testIsSystemAdministrator() {
            // 1. 构建权限记录,作为DAO层方法的、指定的返回值
            Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
            // 2. 使用Mockit模拟出一个mapper
            AuthorityMapper mapper = mock(AuthorityMapper.class);
            // 3. 设置访问方法的返回值
            when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(authority);
            // 4. 创建service,传入mock出来的mapper
            AuthorityServiceImpl service = new AuthorityServiceImpl();
            service.setAuthorityMapper(mapper);
            // 5. 访问方法,将调用AuthorityMapper的getByUserAndProject()方法,返回指定的authority
            // 从而使得isSystemAdministrator()判断的结果为true
            Assert.assertTrue(service.isSystemAdministrator("sunrise"));
        }
    }
    
  • JUnit和Mockit框架中,很多方法都是静态方法,为了使得的代码简洁,通过import static进行引入。
  • 为了节省篇幅,后续代码示例,将不再给出引用static方法的import语句

  • 一些参考链接:
    • Unit Test – What is Mocking? and Why?(为什么使用Mockit模式外部服务,娓娓道来)
    • SpringBoot使用Junit测试 防止事物自动回滚(关于注解@Transactional的使用)
    • JUnit 5 tutorial - Learn how to write unit tests(JUnit5使用教程)

2. 关于单元测试

  • 《Best Practices For Unit Testing In Java》中,是这样描述单元测试的:
    • 单元测试是软件设计和实现中的关键步骤,它不仅可以提高代码的效率和有效性(efficiency and effectiveness,翻译可能不是很准确),还使得代码更加健壮(robust),减少了以后开发和维护中的回归(regression)-
    • 通过创建不同的测试用例,以验证每个独立的源代码单元的表现
  • 博文提出的关于单元测试的一些最佳实践包括但不限于:
    • 测试代码的开发、执行和维护,都应该与生产代码独立开来。例如,在maven项目中,测试代码存放在src/main/test,生产代码存放在src/main/java
    • 测试代码的包名,应该与生产代码的包名保持一致
    • 测试名应该是有洞察力的(insightful),用户只需要看一眼测试名就能了解该测试的行为和期望(容易导致方法名很长,感觉很难做到)
    • 期望值和真实值,可以使用合适的断言(assert)进行判断。例如,使用JUnit中的Assert.assertTrue()判断boolean类型,使用Assert.assertEquals()判断数值、字符串或其他自定义类
    • 尽量保证一个单元测试,只测试一种特定场景。
      • 例如,测试用户是否为系统管理员,三种场景可以编写三个单元测试
      • 这样可以避免单元测试变得复杂而难以理解,还有利于后续调试和维护单元测试。
    • 使用Mockit、EasyMock、JMockit等模拟(mock)外部服务,只关注被测代码在不同场景下的执行逻辑
    • 对于相似的单元测试,对数据或外部服务的mock,可以提取到一个方法中,避免代码冗余
    • 单元测试应该覆盖 80% 的生产代码
    • … …, 详情见《Best Practices For Unit Testing In Java》

3. Mockit

3.1 mock & test doubles

mock的重要性

  • mock(模拟)应该是测试领域的一个概念,即使用替身(doubles)替代真实的对象
  • 我们可以控制替身的行为,从而达到mock真实对象的目的
  • 就像本文的开头,想要判断一个用户是否为系统管理员,需要访问MySQL数据库。
  • 如果访问一个真实的数据库,费时费力、还不保证后续的稳定性。
  • 这时,我们可以模拟出一个Mapper对象,它可以在我们输入特定的条件时,返回特定的结果
  • mock出的Mapper对象,使得单元测试简单、快速且可靠

test doubles

  • mock出的替身,又叫test doubles
  • test doubles有多种类型,每种类型有自己的职责,比较常见的类型包括:stub、mock、spy、fake、dummy等
  • 网上对于这些类型的描述五花八门,让人脑壳发晕
  • 笔者认为,test doubles的类型实际可以狭义地理解为如何使用替身
    • stub:用于返回预定义的值,通常用于模拟数据库
    • spy:一种stub,但还记录了替身被调用的信息,如被调用的次数,可以用于验证替身是否被调用
      • 例如,DAO层的delete()方法没有返回值,单元测试时,只需要关注该替身的delete()方法否被调用
    • mock(名词):根据入参返回期望的值,可以用于验证被测单元是否按照预期与替身进行交互
      • 一旦按照期望的方式调用了替身,替身将返回一个指定的值
  • 对于上述三种替身,博客《stub fake spy mock区别》给出的伪代码,可能比较符合笔者的理解
  • PS:
    • 笔者直到使用Mockit进行单元测试时,也没有很好地分清stub和mock的区别 😂
    • 只能根据实际需求,知道自己该使用哪些Mockit函数定义替身的行为

  • 关于test doubles各种类型的讲解,可以参考如下链接:
    • 基于Spock介绍mock和stub:spock测试桩mock和stub的区别及使用场景
    • Martin Fowler大佬的文章:Mocks Aren’t Stubs

3.2 如何使用Mockit创建替身?

3.2.1 使用Plain Mockit

  • 所谓的Plain Mockit,就是使用Mockit的 静态 方法mock()创建一个替身

  • 例如,前面创建AuthorityMapper替身时,就是使用的plain Mockit

    AuthorityMapper mapper = Mockit.mock(AuthorityMapper.class);
    

3.2.2 使用Mockit注解

  • 下面的代码展示了,如何使用@Mock注解创建替身

  • 注意: 使用@Mock注解只是标识这是一个替身,还需要通过MockitoAnnotations.initMocks()初始化替身

    // 必须添加该注解,否则替身无法初始化,使用时将抛出NullPointerException
    @RunWith(MockitoJUnitRunner.class) 
    public class MyAuthorityServiceTest {
        @Mock
        private AuthorityMapper mock; // 只是添加了@Mock注解,并没有创建替身
    
        @Before // 替身的初始化,需要在测试类的启动方法中完成,因此使用@Before标识setUp()方法
        public void setUp() {
        	// initMocks()负责为当前类中,添加了@Mock注解的字段或入参创建替身
            MockitoAnnotations.initMocks(this); 
        }
    
        @Test
        public void testIsSystemAdministrator() {
            Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
            when(mock.getByUserAndProject("sunrise", "system")).thenReturn(authority);
    
            AuthorityServiceImpl service = new AuthorityServiceImpl();
            service.setAuthorityMapper(mock);
    
            Assert.assertTrue(service.isSystemAdministrator("sunrise"));
        }
    }
    
  • 注意: @RunWith(MockitoJUnitRunner.class)是替身成功初始化的关键

  • 感谢Stack Overflow的提问:mock instance is null after @Mock annotation

3.2.3 使用JUnit Jupiter的MockitoExtension

  • 使用@Mock注解仍然无法实现替身的自动创建,JUnit Jupiter的MockitoExtension可以实现

  • 具体代码如下:

    @RunWith(MockitoJUnitRunner.class)
    @ExtendWith(MockitoExtension.class)
    public class MyAuthorityServiceTest {
        @Mock
        private AuthorityMapper mock;
    
        @Test
        public void testIsSystemAdministrator() {
            Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
            when(mock.getByUserAndProject("sunrise", "system")).thenReturn(authority);
    
            AuthorityServiceImpl service = new AuthorityServiceImpl();
            service.setAuthorityMapper(mock);
    
            Assert.assertTrue(service.isSystemAdministrator("sunrise"));
        }
    }
    
  • 注意: 需要将JUnit从4升级到5,否则单元测试会报错java.lang.NoClassDefFoundError: org/junit/jupiter/api/extension/ScriptEvaluationException

    <!-- MockitoExtension对应的依赖 -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>2.17.0</version>
        <scope>test</scope>
    </dependency>
    <!-- JUnit 5对应的依赖 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.2.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-runner</artifactId>
        <version>1.2.0</version>
        <scope>test</scope>
    </dependency>
    

  • 感谢stack overflow中的answer给我灵感, Junit Test error : org/junit/jupiter/api/extension/ScriptEvaluationException
  • 关于如何将JUnit从4升级到5,参考博客:JUnit Setup Maven - JUnit 4 and JUnit 5

4. 如何使用Mockit定义替身的行为?

  • 替身实际就是一个对象,要想替身在测试中发挥作用,就得在调用替身的某个方法时,替身能做出一定的响应
  • 例如:
    • 有返回值的方法,调用该方法时,需要返回一个值。
    • 没有返回值的方法,调用该方法时,希望能通过统计方法的调用次数,确定该方法是否被调用

4.1 when().thenReturn():让替身返回指定值

  • 1.4 中,使用when().thenReturn(),定义了替身在getByUserAndProject()方法的行为:以入参sunrise、system调用getByUserAndProject()方法,则返回指定的权限记录
    when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(authority);
    
  • when(x).thenReturn(y)的含义:When the x method is called then return y
  • 在没有 overwrite 替身行为的情况下,只要getByUserAndProject()方法的入参为sunrise、system,多次调用该方法都将返回同样的权限记录

4.1.1 定义多个返回值

  • 有时,我们希望连续调用替身的方法时,替身能展现出不同的行为(如返回不同的值)

  • 这时,如果使用多个when().thenReturn();,代码将显得非常冗余

  • 希望能像Builder模式设置属性一样,能一次定义多个返回值 —— Mockit支持该特性

    @Test
    public void isSystemAdministratorTest() {
        Authority authority1 = new Authority("sunrise", Role.SYSTEM, "system", "jack");
        Authority authority2 = new Authority("sunrise", Role.NORMAL, "system", "jack");
    
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        // 定义不同的返回值,以便在连续方法调用中返回不同的值
        when(mapper.getByUserAndProject("sunrise", "system"))
                .thenReturn(authority1)
                .thenReturn(authority2)
                .thenReturn(null);
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
    
        // 测试sunrise为系统管理员的情况
        assertTrue(service.isSystemAdministrator("sunrise"));
        // 测试sunrise权限不足,不是系统管理员的情况
        assertFalse(service.isSystemAdministrator("sunrise"));
        // 测试sunrise的信息不存在的情况
        assertFalse(service.isSystemAdministrator("sunrise"));
        // 注意: 后续的调用,替身都将返回null
        assertFalse(service.isSystemAdministrator("sunrise"));
    }
    
  • 除了上述这种多次thenReturn()的写法,还可以精简为一个thenReturn()

    when(mapper.getByUserAndResource("sunrise", "system"))
                    .thenReturn(authority1, authority2, null);
    

4.1.2 替身方法的默认返回值

  • 上面的代码示例,调用替身方法时,都使用了符合要求的入参,因此都能返回thenReturn()中指定的值

  • 如果方法入参不满足when()中规定的条件,则将根据方法的返回值类型,返回一个默认值

    • 针对数值类型的原子/原子包装类型,如int/Integer,将返回0
    • 针对布尔类型的原子/原子包装类型, boolean/Boolean,将返回false
    • 其他类型将返回null
  • 下面的代码,展示了替身方法的默认返回值

    @Test
    public void defaultReturnValueTest() {
        Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        // 返回值为Authority类型
        when(mapper.getByUserAndResource("sunrise", "system")).thenReturn(authority);
        // 入参不符合要求,返回默认值null
        assertNull(mapper.getByUserAndResource("jack", "test-project"));
    
        // insert()方法的返回值为int类型,即affected rows
        when(mapper.insert(authority)).thenReturn(1);
        // 入参不符合要求,返回默认值0
        assertEquals(0, mapper.insert(new Authority()));
    }
    

4.1.3 不关心方法入参的具体值

  • 在进行单元测试时,我们不关心方法入参的具体值,只要类型符合要求,替身都能返回相同的值

  • 例如,DAO层的insert操作,无论插入什么样的记录,默认成功插入,返回affected rows为1

  • 这时候可以使用ArgumentMatchers提供的各种静态any()/anyX()方法

    • any(Class<T> type):匹配任何指定Class类型的入参,不包括null
    • anyObject():允许任何对象作为入参,包括null
    • 对于基本数据类型:anyByte()anyChar()anyInt()
    • anyString():允许任何String类型的入参,不包括null
  • 对service的insert()方法进行单元测试,使用any(Authority.class)表示只要是Authority类型的入参都符合要求

    // 单元测试的代码
    @Test
    public void insertTest() {
        Authority authority1 = new Authority("sunrise", Role.SYSTEM, "system", "jack");
        // NewAuthority extends Authority
        Authority authority2 = new NewAuthority("john", Role.SYSTEM, "test_project_1", "jack");
    
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        // 只要插入Authority,都统一返回1,表示成功插入数据
        when(mapper.insert(any(Authority.class))).thenReturn(1);
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
    
        // 成功插入authority1
        assertTrue(service.insert(authority1));
        // 成功authority2
        assertTrue(service.insert(authority2));
        // 插入null,不是Authority类型,将返回默认值0,导致insert操作失败
        assertFalse(service.insert(null));
    }
    
    // service.insert()方法的实现
    @Override
    public boolean insert(Authority authority) {
        int row = authorityMapper.insert(authority);
        if (row == 1) {
            return true;
        }
        return false;
    }
    

4.2 让替身抛出异常

  • 单元测试时,除了希望替身能返回指定值,还希望替身能抛出异常,以测试目标方法能否处理异常

  • 这时,可以使用when().thenThrow()让替身抛出异常

  • 下面的代码示例,让替身的insert()方法抛出异常,最终该异常将被service层的insert()方法上抛

    @Test(expected = RuntimeException.class)
    public void insertTest() {
        Authority authority = new Authority("lucy", Role.SYSTEM, "system", "jack");
    
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        // 插入的权限记录,user字段过长,触发异常
        when(mapper.insert(authority)).thenThrow(new RuntimeException("Unexpected error when insert data"));
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
        // service的insert()方法,没有进行异常处理,DAO层的异常将上抛
        service.insert(authority);
    }
    

4.2.1 无返回值的方法如何抛出异常?使用 doThrow()

  • AuthorityMapper有一个delete方法,定义如下:

    void deleteData(long id);
    
  • service的delete方法定义如下,调用了AuthorityMapper的delete方法:

    @Override
    public void delete(long userId) {
        authorityMapper.deleteData(userId);
    }
    
  • 使用when().thenThrow()让mapper替身在执行delete方法时抛出异常,此时发现IDE提示代码编写错误
    在这里插入图片描述

  • 查看when()方法的源码,发现它是一个泛型方法,会返回一个包含被调方法的OngoingStubbing<T>对象

    @CheckReturnValue
    public static <T> OngoingStubbing<T> when(T methodCall) {
        return MOCKITO_CORE.when(methodCall);
    }
    
  • 对于返回值为void的方法,是不能使用when()定义调用条件的

  • 对于返回值为void的方法,可以使用doThrow(exception).when(testDoubles).methodCall()来定义抛出异常

    @Test(expected = RuntimeException.class)
    public void deleteTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
    
        // deleteData()返回值为void,使用doThrow()定义异常
        doThrow(new RuntimeException("Unexpected error when delete data")).when(mapper).deleteData(anyLong());
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
        // service的delete()方法,没有进行异常处理,异常将上抛
        service.delete(1024);
    }
    

4.2.2 定义多个返回值和异常

  • when().then...除了支持定义多个返回值,还允许定义多个异常,甚至还能返回值和异常一起定义
    • 一次定义多个异常:

      @Test
      public void isSystemAdministratorTest() {
          AuthorityMapper mapper = mock(AuthorityMapper.class);
          // 定义多个异常
          when(mapper.getByUserAndProject("sunrise", "system"))
                  .thenThrow(new RuntimeException("Unexpected error"))
                  .thenThrow( new SecurityException("Can not modify the database"));
          // 等同于如下语句
          /* when(mapper.getByUserAndResource("sunrise", "system"))
                  .thenThrow(new RuntimeException("Unexpected error"), new SecurityException("Can not modify the database")); */
          
          AuthorityServiceImpl service = new AuthorityServiceImpl();
          service.setAuthorityMapper(mapper);
      
          try {
              service.isSystemAdministrator("sunrise");
          } catch (RuntimeException exception) {
              assertEquals("Unexpected error", exception.getMessage());
          }
          try {
              service.isSystemAdministrator("sunrise");
          } catch (SecurityException exception) {
              assertEquals("Can not modify the database", exception.getMessage());
          }  
      }
      
    • 返回值和异常一起定义

      @Test
      public void isSystemAdministratorTest() {
          AuthorityMapper mapper = mock(AuthorityMapper.class);
          // 返回值和异常一起定义, 无法精简到一个语句中
          when(mapper.getByUserAndProject("sunrise", "system"))
                  .thenReturn(null)
                  .thenThrow( new SecurityException("Can not modify the database"));
      
          AuthorityServiceImpl service = new AuthorityServiceImpl();
          service.setAuthorityMapper(mapper);
      
          assertFalse(service.isSystem("sunrise"));
          try {
              service.isSystem("sunrise");
          } catch (SecurityException exception) {
              assertEquals("Can not modify the database", exception.getMessage());
          }
      }
      

4.2.3 如何覆盖以前的异常行为?

  • 一次偶然的机会,将异常和返回值分开定义了,竟然运行报错

    // 异常和返回值的定义分开,其余代码与上一个示例保持一致
    when(mapper.getByUserAndProject("sunrise", "system"))
            .thenThrow(new SecurityException("Can not modify the database"));
    when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(null);
    
  • 出错的代码行号,竟然是when().thenReturn()语句
    在这里插入图片描述

  • 后来仔细阅读了Mockit的代码注释,发现需要使用doReturn().when()替代when().thenReturn(),已覆盖之前定义的的异常行为

    @Test
    public void isSystemAdministratorTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        // 返回值和异常分开定义
        when(mapper.getByUserAndResource("sunrise", "system"))
                .thenThrow(new SecurityException("Can not modify the database"));
        doReturn(null).when(mapper).getByUserAndProject("sunrise", "system");
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
    
        try {
            service.isSystem("sunrise");
        } catch (SecurityException exception) {
            logger.info(exception.getMessage()); // 稍微有点改动,直接打印异常
        }
        assertFalse(service.isSystem("sunrise"));
    }
    
  • 运行代码,发现未打印异常信息。

  • 原因: 使用doReturn().when()替代when().thenReturn(),成功覆盖了之前定义的异常行为,使得后续调用不会再抛出异常

  • 注意:

    • 通过when().then...一次定义多个行为时,异常和返回特定值的行为,是按照顺序出现的
    • 而如果多次使用通过when().then...定义替身行为,后面的行为将覆盖前面的行为
    • 特殊情况: 想要覆盖之前定义的异常行为,需要使用doReturn().when(),而非when().thenReturn()

4.3 校验方法的调用次数

4.3.1 常规用法

  • 对于返回值为void的方法,定义替身行为时,我们更加关心该方法是否被成功调用

  • 因为返回值为void,无需通过thenReturn()doReturn()让其返回特定值

  • 这时,可以使用verify()验证void方法的调用次数。默认为一次,还可以通过times()指定调用次数

    @Test
    public void deleteTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
    
        service.delete(1024);
        // deleteData()返回值为void,使用verify()校验调用次数,默认为一次
        verify(mapper).deleteData(1024);
        // 还可以指定调用次数
        verify(mapper, times(0)).deleteData(256);
        // 等价于下面的调用
        verify(mapper, never()).deleteData(256);
    }
    
  • 注意:verify()不仅局限于void方法,非void方法也可以使用

4.3.2 规定调用次数的上限、下限

  • 有时,方法的调用次数不是确定,我们只知道调用次数的上限或者下限

  • 这时,可以使用atMost()atLeast()进行限制

    @Test
    public void deleteTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
    
        AuthorityServiceImpl service = new AuthorityServiceImpl();
        service.setAuthorityMapper(mapper);
    
        service.delete(1024);
        // 调用替身的deleteData(1024)方法至多一次
        verify(mapper, atMost(1)).deleteData(1024);
        // 等价于下面的语句
        verify(mapper, atMostOnce()).deleteData(1024);
    
        service.delete(1024);
        // 调用次数是个累积值,此时deleteData(1024)已被调用2次
        verify(mapper, atLeast(2)).deleteData(1024);
        // 至少一次的简写
        verify(mapper, atLeastOnce()).deleteData(1024);
    }
    

4.3.3 校验其他方法未被调用

  • 除了调用指定的方法,我们希望替身的其他方法为被调用,这时可以使用verifyNoMoreInteractions()或者verifyZeroInteractions()

    @Test
    public void verifyTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
    
        mapper.deleteData(1024);
        Authority authority = new Authority("sunrise", Role.ADMIN, "test-project", "jack");
        mapper.insert(authority);
    
        // 校验被调用过的方法
        verify(mapper).deleteData(1024);
        verify(mapper).insert(authority);
        
        // 除了被校验的方法,没有其他方法被调用
        verifyNoMoreInteractions(mapper);
        // 等价于下面的方法
        verifyZeroInteractions(mapper);
    }
    

4.4 其他高级用法

4.4.1 救火英雄:doReturn()

  • 大多数情况下,doReturn().when()when().thenReturn()二者等价,都是让替身返回特定值

  • when().thenReturn()更直白易懂,因此建议使用when().thenReturn()

  • 但在一些特殊情况下,必须使用doReturn().when()替代when().thenReturn()

  • 情况1: doReturn().when()覆盖前面的异常定义,详情见4.2.3

  • 情况2: 使用spy()监视真实对象,在spy上调用真实方法会产生副作用时,需要使用doReturn().when()

  • 例如,使用spy()监视list对象,并在spy上调用get()方法,获取list中的元素

  • 如果使用when().thenReturn(),规定get()方法的返回值,可能会引发错误

    @Test
    public void spyTest() {
        List<Integer> list = new ArrayList<>();
        List<Integer> spy = spy(list);
        // 定义spy的行为
        when(spy.get(0)).thenReturn(10);
        // 调用spy
        assertEquals(Integer.valueOf(10), spy.get(0));
    }
    
  • 执行上面的代码,发现在使用when().thenReturn()定义行为时,就出现了IndexOutOfBoundsException
    在这里插入图片描述

  • 错误原因:

    • 使用spy监视真实对象,使用when().thenReturn()定义spy行为时,将调用被监视对象的真实方法
    • 而真实对象是一个empty list,通过get(0)访问其元素时,将触发IndexOutOfBoundsException
  • 正确做法: 使用doReturn().when()替代when().thenReturn()

    @Test
    public void spyTest() {
        List<Integer> list = new ArrayList<>();
        List<Integer> spy = spy(list);
        // 定义spy的行为
        doReturn(10).when(spy).get(0);
        // 调用spy
        assertEquals(Integer.valueOf(10), spy.get(0));
    }
    

4.4.2 thenAnswer()或让替身行为更复杂

  • thenReturn()doReturn(),只能让替身返回特定的值

  • 我们希望替身返回的值与方法入参有关系,或者随着方法调用而变化

  • 例如,DAO层的insert()方法,返回自增的主键id的值;每次调用,返回的值应该有所变化

  • 这时,可以使用thenAnswer()让替身行为更复杂

    @Test
    public void insertTest() {
        Authority authority = new Authority("lucy", Role.SYSTEM, "system", "jack");
    
        AuthorityMapper mapper = mock(AuthorityMapper.class);
        AtomicInteger id = new AtomicInteger();
        // insert方法,返回递增的id主键值
        when(mapper.insert(authority)).thenAnswer(new Answer<Integer>() {
            @Override
            public Integer answer(InvocationOnMock invocation) throws Throwable {
                Authority input = invocation.getArgument(0); // 获取入参
                System.out.println("成功插入数据: " + input); // 打印入参
                return id.incrementAndGet(); // 返回自增的主键id的值
            }
        });
        // 第一次调用,主键值为1
        assertEquals(1, mapper.insert(authority));
    }
    
  • 对于返回值为void的替身方法,可以使用doAnswer()丰富替身行为

    @Test(expected =  IllegalArgumentException.class)
    public void deleteTest() {
        AuthorityMapper mapper = mock(AuthorityMapper.class);
    
        // lambda表达式实现Answer接口,当id不是1024时,抛出异常;否则,返回null
        doAnswer(invocation -> {
            Long id = invocation.getArgument(0);
            if (id != 1024L) {
                throw new IllegalArgumentException("不存在id为" + id + "的记录");
            }
            System.out.println("成功删除id为" + id + "的记录");
            return null;
        }).when(mapper).deleteData(anyLong()); // 输入值为任意类型
    
        mapper.deleteData(1024L);
        mapper.deleteData(128L); // 触发异常
    }
    

5. 后记

  • 对Mockit的学习暂告一段落
  • 回顾整个学习过程,自己的收获如下:
    1. 学会使用test doubles进行单元测试,可以排除对外部服务的依赖,使得单元测试的简单、快速且可靠
    2. 使用Mockit框架创建替身,定义替身行为(返回特定值、抛出异常)、验证调用次数等
    3. 大多数情况,使用when().thenX()定义替身行为,特殊情况需要转为使用对应的doX().when()
    4. Mockit框架提供了很多有用的函数,例如表示任何值的anyX(),定义调用次数上限或下限的atLeast()、atMost()等
  • 入门级教程:Clean Unit Tests with Mockito
  • 非常完善的教程:Unit tests with Mockito - Tutorial
  • 关键函数的对比理解:
    • Mockito - difference between doReturn() and when()
    • Mockito : doAnswer Vs thenReturn
    • Mockito: Difference between doThrow() and thenThrow()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/60751.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CTF之序列化__toString

序列化简介 本质上serialize()和unserialize&#xff08;&#xff09;在php内部的实现上是没有漏洞的&#xff0c;漏洞的主要产生是由于应用程序在处理对象&#xff0c;魔术函数以及序列化相关问题时导致的。 当传给unserialize()的参数可控时&#xff0c;那么用户就可以注入精…

【应用】Docker Swarm

Docker SwarmDocker Swarm 集群配置配置前准备初始化 SwarmSwarm 常用命令Portainer 集群管理Docker Swarm 集群配置 masternode1node2192.168.86.133192.168.86.131192.168.86.139 配置前准备 关闭各个节点服务器的防火墙 systemctl stop firewalld systemctl disable fire…

ATF问题二则:EL3可能没有实现吗? aarch32中的S-EL1是什么?

最近两个问题&#xff0c;戳到了我的知识盲点&#xff0c;当然我这个菜鸡ATF哪里都是盲点。 问题一&#xff1a;EL3可能没有实现吗&#xff1f; 问题二&#xff1a;bl2是aarch32, 那么bl2是S-EL1&#xff0c;bl31也是S-EL1? 1、EL3可能没有实现吗&#xff1f; The Armv8-A …

基于MATLAB的一级倒立摆控制仿真,带GUI界面操作显示倒立摆动画,控制器控制输出

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 一个可以活动的小车上立着一根不稳定随时会倒下的杆。小车的轮子由电机控制&#xff0c;可以控制小车电机的转动力矩M。同时&#xff0c;也可以获取小车轮子转动的圈数N&#xff08;可以精确到小…

java计算机毕业设计ssm实验室设备管理系统5k648(附源码、数据库)

java计算机毕业设计ssm实验室设备管理系统5k648&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xf…

162.基于Django-rest_framework的身份验证和权限

1. 概述 到目前为止&#xff0c;程序的API对任何人都可以编辑或删除&#xff0c;没有任何限制。我们希望有一些更高级的行为&#xff0c;进行身份验证和权限分配&#xff0c;以确保&#xff1a; 数据始终与创建者相关联只有经过身份验证的用户才能创建数据只有数据的创建者可…

嵌入式Linux上ifpulgd的使用配置与qemu模拟验证

问题引入 最近在项目开发中收到了一个非常简单的需求&#xff0c;我们的嵌入式Linux板卡需要在检测到网口插拔后重新配置网络&#xff0c;这在pc环境中非常常见。但是在这个项目的默认SDK中并没有相关配置&#xff0c;稍微查询了一下&#xff0c;在一般pc上通常使用Ifpulgd,并…

[附源码]Python计算机毕业设计Django企业售后服务管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

【数据结构】树的概念与堆的实现

树的概念与堆的实现1、什么是树1.1 树的概念1.2 树的相关概念1.3 树的表示2、二叉树概念及结构2.1 概念2.2 特殊的二叉树2.3 二叉树的性质2.4 二叉树的存储结构3、二叉树的顺序结构及实现3.1 二叉树的顺序结构3.2 堆的概念及结构3.3 堆的实现3.3.1 创建一个堆3.3.2 初始化堆3.3…

【计算机毕业设计】基于JSP的网上购物系统的设计与实现

分类号&#xff1a;TP315 U D C&#xff1a;D10621-408-(2007)5883-0 密 级&#xff1a;公 开 编 号&#xff1a;2003214012 学位论文 基于JSP的网上购物系统的设计与实现 基于JSP的网上购物系统的设计与实现 摘 要 近年来&#xff0c;随着Internet的迅速崛起&#xff0c…

内存 分页、交换空间

目录 1. 分页 1.1 地址转换 1.2 页表存在哪里 1.3 列表中究竟有什么 1.4 分页的优缺点 2. 快速地址转换&#xff08;TLB&#xff09; 2.1 TLB 的基本算法 2.2 谁来处理 TLB 未命中 2.2.1 硬件处理 2.2.2 软件&#xff08;操作系统&#xff09;处理 2.3 TLB 的内容 …

[附源码]Python计算机毕业设计SSM精准扶贫系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

(免费分享)基于springboot,vue公司财务系统

该系统是一个简单的公司财务管理系统&#xff0c;包含用户基本信息管理&#xff08;员工管理&#xff0c;管理员管理&#xff09;&#xff0c;工资管理&#xff08;员工工资管理&#xff0c;管理员工资管理&#xff09;&#xff0c;业务管理&#xff08;员工业务管理&#xff0…

Windows server 2012搭建用户隔离FTP站点

Windows server 2012搭建用户隔离FTP站点 系统添加FTP功能创建FTP登陆账户和其使用的文件夹D盘根目录下创建FTP站点主目录ftp文件夹ftp下创建用户主目录localuser&#xff08;名称不可更改&#xff0c;实现用户隔离必要步骤&#xff09;Localuser文件夹下创建对应用户的文件夹…

opencv入门笔记(二)

目录图像运算位运算位与运算位或运算取反运算异或运算位运算特点示例&#xff1a;位运算示例加法运算示例&#xff1a;查看三种加法运算的区别滤波器均值滤波中值滤波高斯滤波双边滤波示例&#xff1a;查看多种滤波器的处理效果视频处理示例&#xff1a;打开笔记本电脑内置摄像…

轻量化神经网络(移动设备上的神经网络)的整体框架

提示&#xff1a;不断更新中 文章目录一、为什么要引入轻量化神经网络二、模型压缩(Model Compression)参数修建低秩因子分解参数量化知识蒸馏人工神经架构设计三、自动压缩和神经架构搜索(Automated Compression and Neural Architecture Search)自动模型压缩(Automated Model…

【软件工程】白盒测试:基本路径测试

基本路径测试是在程序控制流图的基础上&#xff0c;通过分析控制构造的环路复杂性&#xff0c;导出基本可执行的路径集合&#xff0c;从而设计测试用例的方法。 步骤(以一段代码为例)&#xff1a; &#xff08;1&#xff09;画出控制流图 void sort(int num,int t) 1. { 2. i…

MySQL数据库的SQL语句

MySQL数据库的SQL语句MySQL的常用数据类型MySQL数据类型及含义char与varchar的区别MySQL的基本命令登录数据库查看MySQL数据库版本查看当前服务器中的数据库查看MySQL库中的表查看表的结构&#xff08;字段&#xff09;SQL语句SQL简介及分类创建新的数据库创建新的表添加表中各…

卡尔曼滤波之基本概念和状态观测器

图裂参考原文&#xff1a;https://www.cnblogs.com/odesey/p/16937124.html 1. 为什么使用 Kalman Filters&#xff1f; 1.1 什么是 Kalman Filters&#xff1f; Kalman Filters 是一种优化估计算法。下面使用使用两个例子说明 Kalman Filters 的用处。 例子1&#xff1a; …

电子学会2021年3月青少年软件编程(图形化)等级考试试卷(二级)答案解析

目录 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 二、判断题&#xff08;共10题&#xff0c;每题2分&#xff0c;共20分&#xff09; 三、编程题&#xff08;共2题&#xff0c;共30分&#xff09; 青少年软件编程&#xff08;图形化&a…