欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单元测试—— Mock 对象行为之 Mockito

程序员文章站 2022-06-01 16:46:28
...

前言

       本章要介绍的工具时 Spring 的单元测试工具之一,Mockito,Mock对象的行为。

       满足单一职责原则的类,都有自己独立的功能与行为,而类与类之间又是相互关联的。就像社会中的人类,人是一个独立的单元,有各自的特征和行为,但人也不可能独立而存在。这种相互关联就是相互间的依赖,要剥离开这种依赖而对单独的一个类进行测试,就必须要有工具来模拟类的依赖,而 Mockito 就是这种工具。

       这里从基于 Spring MVC 的三层架构说起,大多数使用场景是将其分为 Controller层,Service层,DAO层,它们的职责独立而又相互依赖。首先看看它们各自的职责:

       1,Controller层,这一层通常是定义服务的接口,对业务模块流程的控制,它由Spring MVC框架提供支持;

       2,Service层,这一层通常会响应 Controller 的业务请求,并处理业务逻辑。在 Service 层的业务逻辑是比较多样的,包括业务的具体流程的设计,数据层的访问,也可能会访问中间件组件,例如缓存服务器Redis,消息服务RocketMQ,不仅如此,Service层还可能涉及到第三方服务的调用,例如调用支付宝或者微信的支付业务,百度或者高德的地图业务等等之类的接口。所以这层将作为 Unit Test 的重点。

       3,DAO层,Data Access Object,顾名思义,这一层就是用于对数据库的访问与存储的。在简单的设计种,甚至可以把它纳入Service层,但是极其不推荐。单独把它抽离出来是很有道理的,首先在多数据库的设计种,它可以对外部调用屏蔽内部的实现,然后是对驱动或者依赖的升级是很友好的。这一层通常在开发时就设计并测试好了的。

       确定了Spring MVC架构种各部分的职责,就把 Unit Test 的重点放 在Service 层来讲。

       Service层是业务逻辑处理最复杂的一部分,它对外部有很多依赖,如果每次单元测试就将这些依赖配置一遍,那是相当困难的,甚至是不可能的,因此 Spring 提供的 Mockito 就是用来模拟这些依赖的。

Mockito 实践

       首先,引入 Maven 依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.1.6.RELEASE</version>
    <scope>test</scope>
</dependency>

       测试的Demo类,Demo 是记录小狗动作并计分的实现,纯属虚构

package com.zhaoxj_2017.service;

import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class RewardPuppyService {
    private static final String SIT_DOWN = "sitDown";
    private static final String JUMP_UP = "jumpUp";
    private static final String STANDING = "Standing";

    /**
     * 注入 DAO ,需要 Mock
     */
    @Value("puppyActionCol")
    private MongoCollectionService puppyActionService;

    private final Map<String, Integer> rules = new HashMap<>();

    private int totalScore = 0;

    /**
     * 创建 Bean 时需初始化的数据
     */
    @Autowired
    private void initRules() {
        rules.put(SIT_DOWN, 5);
        rules.put(JUMP_UP, 10);
        rules.put(STANDING, 15);
    }

    public int recordPuppyActionAndScore(String puppyId, String action) {
        Document record = new Document();
        record.put("puppyId", puppyId);
        record.put("action", action);
        record.put("score", rules.get(action));
        puppyActionService.insertOne(record);

        totalScore += rules.get(action);

        return totalScore;
    }

    public List<Document> getAllRecord(String puppyId) {
        return puppyActionService.findMany(new Document().append("puppyId", puppyId));
    }
}

       测试类,通过 Mockito 模拟 @Autowired 或者 @Value 的依赖。

package com.zhaoxj_2017.service;

import org.bson.Document;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.test.context.SpringBootTest;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;

@RunWith(MockitoJUnitRunner.class)
// @SpringBootTest 指定测试时需要的类,避免测试时加载不必要的类,也可省略。
@SpringBootTest(classes = {RewardPuppyService.class, MongoCollectionService.class})
public class RewardPuppyServiceTest {
    private static final String SIT_DOWN = "sitDown";
    private static final String JUMP_UP = "jumpUp";
    private static final String STANDING = "Standing";

    private List<Document> puppyActions = new ArrayList<>();

    private String puppyId = UUID.randomUUID().toString();

    public RewardPuppyServiceTest() {
        Document action = new Document().append("puppyId", puppyId)
                                        .append("action", SIT_DOWN)
                                        .append("score", 5);

        puppyActions.add(action);
    }

    /**
     * 需要 Mock 的对象
     */
    @Mock
    private MongoCollectionService puppyActionService;

    /**
     * 被注入 Mock 对象的测试对象
     */
    @InjectMocks
    private RewardPuppyService rewardPuppyService;

    /**
     * 初始化 @Autowired 注释的方法,
     * 由于 MockitoJUnitRunner 并不像 SpringJUnit4ClassRunner 能够构建 Bean,
     * 因此 @Autowired 注释的方法时并不会执行的,这里通过反射完成初始化
     *
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    @Before
    public void initMocks() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method initMethod = RewardPuppyService.class.getDeclaredMethod("initRules");
        initMethod.setAccessible(true);
        initMethod.invoke(rewardPuppyService);
    }

    @Test
    public void testRecordPuppyActionAndScore() {
        // Mock MongoCollectionService.insertOne(Document) 的行为;
        Mockito.doNothing().when(puppyActionService).insertOne(any(Document.class));

        Assert.assertEquals(5, rewardPuppyService.recordPuppyActionAndScore(puppyId, SIT_DOWN));
        Assert.assertEquals(15, rewardPuppyService.recordPuppyActionAndScore(puppyId, JUMP_UP));
        Assert.assertEquals(30, rewardPuppyService.recordPuppyActionAndScore(puppyId, STANDING));
    }

    @Test
    public void testGetAllRecord() {
        // Mock MongoCollectionService.findMany(Document) 的行为;
        Mockito.when(puppyActionService.findMany(any())).thenReturn(puppyActions);

        Assert.assertEquals(puppyActions, rewardPuppyService.getAllRecord(puppyId));
    }
}

总结

Mockito 的功能集非常的多,能够轻松的模拟套件。上述实践能满足大多数的 Unit Test,直接把指定的 Service 类放入模拟的环境中,完全隔离它的依赖而运行,其功能是非常之多的,更多功能大家可以参考《Mockito Javadoc》一起来探究

参考

Tasty mocking framework for unit tests in Java

Mockito Javadoc

Mockito 简明教程