为什么做单测

在对代码进行单元测试过程中,经常会有以下的情况发生:

class A 依赖 class B  
class B 依赖 class C和class D  
class C 依赖 ...  
class D 依赖 ...  

1.被测对象依赖的对象构造复杂
我们想对class A进行单元测试,需要构造大量的class B、C、D等依赖造步骤多、耗时较长的对象,
对于他们的构造我们可以利用mock去构造过程复杂的对象用于class A的测试,
因为我们只是想测试class A的行为是否符合预期,我们并不需要测试依赖对象。

2.被测单元依赖的模块尚未开发完成,而被测对象需要依赖模块的返回值进行测试:
----- 比如service层的代码中,包含对dao层的调用,但dao层代码尚未实现
----- 比如web的前端依赖后端接口获取数据进行联调测试,但后端接口并未开发完成
----- 比如数据库还不能正常使用但是需要测试功能逻辑是否可行。

关于这种情况我们怎么解决呢,采用mockito框架对单独的模块进行从测试,对代码中的依赖进行解耦。它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等,避免为了测试一个方法,却要自行构建整个 bean 的依赖链。

做好单测,慢即是快

对于单元测试的看法,业界同仁理解多有不同,尤其是在业务变化快速的互联网行业,通常的问题主要有,必须要做吗?做到多少合适?现在没做不也挺好的吗?甚至一些大佬们也是存在不同的看法。我们如下先看一组数字:

“在 STICKYMINDS 网站上的一篇名为 《 The Shift-Left Approach to Software Testing 》 的文章中提到,假如在编码阶段发现的缺陷只需要 1 分钟就能解决,那么单元测试阶段需要 4 分钟,功能测试阶段需要 10 分钟,系统测试阶段需要 40 分钟,而到了发布之后可能就需要 640 分钟来修复。”——来自知乎网站节选

对于这些数字的准确性我们暂且持保留意见。大家可以想想我们实际中遇到的线上问题大概需要消耗多少工时,除了要快速找到bug,修复bug上线,还要修复因为bug引发的数据问题,最后还要复盘,看后续如何能避免线上问题,这样下来保守估计应该不止几人日吧。所以这篇文章作者所做的调研数据可信度还是很高的,

缺陷发现越到交付流程的后端,其修复成本就越高。

有人说写单测太耗费时间了,会延长交付时间,其实不然:

1)研测同学大量的往返交互比编写单测的时间要长的多,集成测试的时间被拖长。

2)没经过单测的代码bug会多,开发同学忙于修复各种bug,对代码debug跟踪调试找问题,也要消耗很多精力。

3)后期的线上问题也会需要大量的精力去弥补。

如果有了单元测试的代码,且能实现一个较高的行覆盖率,则可以将问题尽可能消灭在开发阶段。同时有了单测代码的积累,每次代码改动后可以提前发现这次改动引发的其他关联问题,上线也更加放心。单测虽然使提测变慢了一些,软件质量更加有保障,从而节省了后续同学的精力,从整体看其实效率更高。

所以做好单测,慢即是快。

做为一名开发者我们需要对自己的代码质量负责,也更能体现我们开发者的工匠精神。

单元测试

类注释

1. @SpringBootTest()

2. @ExtendWith(MockitoExtension.class) —— Junit5

3. @RunWith(SpringRunner.class) —— Junit4

Junit注解

mockito

英文文档官网

中文文档官网

常用注解

单测常用

@Mock: 用于创建一个 mock 对象,但不将其注入到 Spring 应用程序上下文中。
用途:用于创建一个 mock 对象,但不将其注入到 Spring 应用程序上下文中。
特点:
a. 适用于单测场景,不能将mock对象注入到Spring 上下文。

b. 不会执行对象的方法(即使方法报错,test指挥使用设置的返回值,不影响流程)
需要手动注入到测试类或方法中。

c. 通常与 @InjectMocks 一起使用,以便将 mock 对象注入到被测试的类中。


@Spy: 创建一个mock  spy对象,即一个真实的对象。会真实的执行对象的方法(若方法报错,test直接报错)。也可以对方法结果直接mock,不关注方法逻辑(不真实执行方法)。
用途:对于对象部分方法真实执行,部分方法mock结果的场景,可以用@Spy实现。
特点:
a. 适用于单测场景,不会将spy对象注入到spring上下文。

b. spy对象为真实对象,会真实的执行对象的方法(若方法报错,test直接报错)。

c. 可以通过doReturn(结果).when(mock的类).方法名(参数)方式对spy对象的部分方法mock 的场景,对方法直接mock不执行真实方法逻辑。

如:doReturn(planContext).when(mockPlanContextFactory).initContext(any()),直接返回的是planContext 不关注方法逻辑,不会报错。


@InjectMocks: 创建一个被测试的类的实例,并将带有 @Mock 或 @Spy 注解的 mock 对象注入到该实例中。
用途:创建一个被测试的类的实例,并将带有 @Mock 或 @Spy 注解的 mock 对象注入到该实例中。
特点:
a. 适用于需要将 mock 对象注入到被测试的类中的场景。

b. 自动将 mock 对象注入到被测试类的构造函数、字段或 setter 方法中。

c. 注释的是要测试的实现类,不是接口 

集成常用

@MockBean: 用于在 Spring Boot 测试环境中创建并注入一个 mock 的 bean。
用途:在 Spring Boot 集成测试中创建一个 mock 的 bean,并将其注入到 Spring 上下文中。
特点:
a. 适用于集成测试,特别是在使用 @SpringBootTest 注解的测试类中。

b. 替换掉 Spring 容器中已有的 bean,或者添加一个新的 mock bean。

与在测试类中直接使用 @Autowired 或@Resource 注解来注入这个 Bean一样的效果。

@SpyBean:专门用于将 spy 对象注入到 Spring 上下文中,用于替换@Spy,确保spy对象被正确的注入spring上下文

用途:

特点:在spring boot集成测试中创建一个spy对象,并将其注入到Spring 上下文。

a. 适用于集成测试,特别是在使用 @SpringBootTest 注解的测试类中

b. 可以通过doReturn(结果).when(mock的类).方法名(参数)方式对spy对象的部分方法进行mock的场景,对方法直接mock不执行真实方法逻辑

c. @MockBean和@SpyBean常与@Autowried或@Resource结合使用

常用方法

mock:构建一个我们需要的对象;可以mock具体的对象,也可以mock接口
spy:构建监控对象
verify:验证某种行为
when:当执行什么操作的时候,一般配合thenXXX 一起使用。表示执行了一个操作之后产生什么效果
doReturn:返回什么结果
doThrow:抛出一个指定异常
doAnswer:做一个什么相应,需要我们自定义Answer
times:某个操作执行了多少次
atLeastOnce:某个操作至少执行一次
atLeast:某个操作至少执行指定次数
atMost:某个操作至多执行指定次数
atMostOnce:某个操作至多执行一次
doNothing:不做任何处理
doReturn:返回一个结果
doThrow:抛出一个指定异常
doAnswer:指定一个操作,传入Answer
doCallRealMethod:返回真实业务执行的结果,只能用于监控对象

常用参数


anyInt:任何int类型的参数,类似的还有anyLong/anyByte等等。

any:任何类型的参数
eq:等于某个值的时候,如果是对象类型的,则看toString方法
isA:匹配某种类型
matches:使用正则表达式进行匹配


常用返回

thenReturn:指定一个返回的值
thenThrow:抛出一个指定异常
then:指定一个操作,需要传入自定义Answer
thenCallRealMethod:返回真实业务执行的结果,只能用于监控对象

mockito使用

引入依赖

<!-- Springboot提供的单测框架,提供一些单测工具支持,默认支持Mockito、junit5 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.5.4</version>
</dependency>

<!-- 或单独引入 -->
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.7.2</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>3.9.0</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>3.9.0</version>
  <scope>compile</scope>
</dependency>

单元使用

通过idea的Squaretest插件直接生成的测试类如下

@ExtendWith(MockitoExtension.class)
public class MockUserServiceTest {

    @Mock
    private UserManager mockUserManager;

    @InjectMocks
    private MockUserService mockUserService;

    /**
    * 只是创建了一个 spy 对象,但不会自动将其注册到 Spring 上下文中
    */
    @Spy
    private PlanContextFactory mockPlanContextFactory;

    @BeforeEach
    public void setUp() {
        mockUserService = new MockUserService(mockUserManager);
    }

    @Test
    public void testGetUserByAge() {
        // Setup
        when(mockUserManager.findByAge(0)).thenReturn(Arrays.asList(
                new User(0, "name", 0),
                new User(1, "name", 0)
        ));
        // Run the test
        final List<User> result = mockUserService.getUserByAge(0);

        // Verify the results
    }

    @Test
    public void testGetUserByAge_UserManagerReturnsNoItems() {
        // Setup
        when(mockUserManager.findByAge(0)).thenReturn(Arrays.asList(
                new User(0, "name", 0)
                , new User(1, "name", 1)
        ));

        // Run the test
        final List<User> result = mockUserService.getUserByAge(0);

        // Verify the results
        assertThat(result).isEqualTo(Collections.emptyList());
    }
}

需注意Junit5.x 与Junit4.x 生成的测试类中,Junit4的测试类和测试方法必须要public关键字修改

因为JUnit 4.x使用Java反射机制来查找和运行测试,而Java反射要求被访问的类和方法必须是public的。

JUnit 5.x(也称为Jupiter)在设计和实现上更加现代化,它引入了一些新的特性和改进,包括更灵活的测试发现机制。在JUnit 5.x中,测试类和测试方法的访问修饰符要求更加宽松。

将测试方法和类声明为public也有助于确保它们能够被其他测试框架或工具(如Maven、Gradle、IDE等)正确地发现和运行。因此,在编写JUnit测试时,即使JUnit 5.x允许更宽松的访问修饰符,但将测试类和测试方法声明为public仍然是一个好习惯

集成使用

springboot集成测试旨在验证Spring Boot应用程序的各个组件之间的交互和整体行为。集成测试非常重要,因为它可以帮助开发人员确保应用程序在不同的环境中都能正常运行。通过集成测试,可以检测应用程序中的潜在问题,提高代码的可靠性和稳定性。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MockInjectServiceImplTest{    

    /**
     * 通过@MockBean的方式创建一个Mock的MockRpcService的Bean
     * 并将其注入到spring的上下文中
     */
    @MockBean
    private MockRpcService mockRpcService;

    /**
     *  通过@SpyBean 注解创建一个spy真实对象,专门用于将 spy 对象注入到 Spring 上下文中。你可以将 @Spy 替换为 @SpyBean,以确保 spy 对象也被正确注入到 Spring 上下文中
     */

    @SpyBean
    private SourceDataManager sourceDataManager;

    @Resource
    private MockInjectService mockInjectService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        ReflectionTestUtils.setField(mockInjectService, "systemEnv", "{\"key\", \"value\"}");
        when(mockRpcService.queryCardNo(anyString())).thenReturn("cardNo");
        /**
         * 1、when(...).thenReturn(...) 语法:
         * 这种语法在某些情况下可能会被 Mockito 误认为是在调用 toString 方法,特别是当 mockRpcService 对象的 toString 方法被重写时。
         * 若在使用 Mockito 模拟这个接口时遇到了 WrongTypeOfReturnValue 异常,这通常意味着 Mockito 误认为你在调用 toString 方法而不是 queryMockResp 方法
         * 如果 mockRpcService 的 toString 方法返回 MockResp 类型,那么 Mockito 会抛出 WrongTypeOfReturnValue 异常。
         *
         * 2、doReturn(...).when(...) 语法:
         * 这种语法更加明确,直接指定了方法的返回值,避免了类型不匹配的问题。适用于所有需要模拟方法返回值的场景。
         * 为了确保代码的健壮性和可读性,建议使用 doReturn(...).when(...) 语法。
         *
         *
         *
         * 下面的例子 使用when(...).thenReturn(...)时 抛出了org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
         * MockResp cannot be returned by toString() toString() should return String
         * 这样的异常。
         */
        //when(mockRpcService.queryMockResp(any(MockReq.class))).thenReturn(MockRespReflection.getMockResp());
        doReturn(MockRespReflection.getMockResp()).when(mockRpcService).queryMockResp(any(MockReq.class));
        doReturn(MockRespReflection.getMockRespList()).when(mockRpcService).getMockRespList(anyInt());

    }

    
    @Test
    public void testGeneralDeal(){
        // 执行被测方法
        MockReq mockReqInput1 = new MockReq();
        mockReqInput1.setName("True-Person");
        MockResp mockRespResult = mockInjectService.generalDeal(mockReqInput1);
        log.info("mockResp:{}", JSON.toJSONString(mockRespResult));
        // 结果比对断言
        Assert.assertNotNull(mockRespResult);
    }



    @Test
    public void testInjectDeal() {
        // 执行被测方法
        MockReq mockReqInput1 = new MockReq();
        mockReqInput1.setName("True-Person");
        MockResp mockRespResult = mockInjectService.injectDeal(mockReqInput1);
        // 结果比对断言
        Assert.assertNotNull(mockRespResult);
    }

    @Test
    public void testBeautifulDeal() {
        // Setup
        final MockResp mockResp = new MockResp("cardNo", 0, false);

        // Run the test
        final String result = mockInjectService.beautifulDeal(mockResp);

        // Verify the results
        assertThat(result).isEqualTo("result");
    }

    

    @Test
    public void testVoidDeal() {
        // Setup
        final MockReq req = new MockReq();
        req.setName("name");
        // Run the test
        mockInjectService.voidDeal(req);


    }
}

以上示例,通过@MockBean创建一个Rpc服务MockRpcService的mock实例,可以对接口的相关方法通过when(...).thenReturn(...) 或doReturn(...).when(...)语法mock。

需注意when(...).thenReturn(...)语法在某些情况下可能会被 Mockito 误认为是在调用 toString 方法,特别是当 mockRpcService 对象的 toString 方法被重写时

而doReturn(...).when(...) 语法更加明确,直接指定了方法的返回值,避免了类型不匹配的问题。适用于所有需要模拟方法返回值的场景。建议使用 doReturn(...).when(...) 语法

RPC接口MockRpcService

**
 * Mockito框架研发场景-RPC接口
 */
public interface MockRpcService {

    
    String queryCardNo(String name);

    
    MockResp queryMockResp(MockReq req);


    
    public List<MockResp> getMockRespList(Integer age);

通过MockRespReflection类中的静态方法 对RPC接口的方法数据进行mock,可以采用直接字符串、文件等形式提前准备数据,这里采用读取文件形式进行mock

ublic class MockRespReflection {


    public static MockResp getMockResp() {
        try {
            String json = new String(Files.readAllBytes(Paths.get("src/test/file/xxx.json")));
            return JSON.parseObject(json, new TypeReference<MockResp>(){});
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 从指定的JSON文件中读取并解析MockResp对象列表
     *
     * @return 解析后的MockResp对象列表
     * @throws RuntimeException 如果读取文件时发生IO异常,将其包装成RuntimeException抛出
     */
    public static List<MockResp> getMockRespList() {
        try {
            // 读取JSON文件内容并解析为MockResp对象列表
            String json = new String(Files.readAllBytes(Paths.get("src/test/file/mockRespList.json")));
            return JSON.parseObject(json, new TypeReference<List<MockResp>>(){});
        } catch (IOException e) {
            // 捕获IO异常并将其包装成RuntimeException抛出
            throw new RuntimeException(e);
        }
    }
}

通过以上配置就可以进行springboot流程的集成测试。Spring Boot集成测试是确保应用程序正确性和可靠性的重要手段。通过上述实践,可以有效地进行集成测试并提高代码质量。

下一篇:【mockito高级篇】mock私有、静态、变量、异常等场景

参考:

一台不容错过的Java单元测试代码“永动机”-CSDN博客

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐