Mockito+Junit单元测试快速入门

1. 背景

我个人认为对于是否要写单元测试这件事情走极端都是不对的,即,【支持方】强制要求必须要写单元测试,强制要求代码单元测试代码覆盖率XX%以上。【反对方】花了甚至比写代码还多的时间去写单元测试然后卵用没有(springboot+mybatis+redis堆业务那代码可不写的快吗?然后由于之前没有写过单元测试,因此导致花的时间更多)。

对于单元测试,个人认为正确的态度是:不强制要求覆盖率一定达到多少,也不认同完全不写单元测试。毕竟通过写单元测试至少可以获得如下收益或者便利:

  • 自测及验证复杂的分支逻辑是否符合预期
  • 在不具备联调的阶段,可以通过单元测试去检验代码的运行情况
  • 在单元测试的编写过程中反思自己的业务代码是否合理并进行重构,从而使得代码更加清晰和优雅(因为有时你会发现由于代码的各种杂糅,导致其单元测试代码相当难写或者别扭,于是主动的去进行重构)。

本文通过一个自己现编的Dog对象的单元测试实例,将Mockito+Junit单元测试的一些常用知识点和方法的使用进行演示,不过实例展示之前还是有必要先讲讲单元测试的基础知识点。

2. 基础知识

2.1 Mock(模拟)

mock这个词硬要去翻译的话好像也找不到合适的词,因此简单用模拟去表达。因为在单元测试的编写过程中,一个最基本的要求是单元测试本身的纯粹性,即我只想测试某一个类或者某一个方法,然而多数的时候情况往往不单纯,例如,目标测试类中依赖了很多其他类及这些类的方法及这些方法的运行结果(要使得测试目标类能跑起来,有时需要mock这些依赖)。另外,单元测试还会要求尽快跑完(要求严格的公司,会在每次build之前跑一次全量单元测试),不因为单元测试而产生真实的数据等(mock dao层对象和其方法即可)。基于这些我们一定要去mock,可以说写单元测试一定要会mock,并且往往是一顿mock猛如虎。

2.1.1 对象Mock

1、无参构造函数对象Mock

  • @InjectMocks
    创建一个目标测试类的mock实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
  • @Mock
    创建一个类的mock实例,一般的情况下目标测试类的mock实例用@InjectMocks去构建,其依赖对象mock实例用@Mock。
  • 手工mock
    创建一个类的mock实例,例如:Mockito.mock(xxClass.class),一般情况下局部对象的mock使用该种方法。

2、有参构造函数对象Mock

// mock示例:
try (MockedConstruction<xxClass> mocked = Mockito.mockConstruction(xxClass.class,
		Mockito.withSettings().useConstructor(args...),
		(mock, context) -> {
			// do something,例如其方法mock
		})) {
	// do something,例如依赖该对象的单元测试逻辑
}

需要try-with-resources的原因为:返回类型是一个MockedStatic对象,它是一个作用域模拟对象。mock的静态方法仅影响创建此静态模拟的线程,并且从另一个线程使用此对象是不安全的。当调用close() 方法时,静态模拟将会释放。如果此对象从未关闭,则静态模拟将在启动线程上保持活动状态。因此,建议在try-with-resources的语句中创建此对象,或者使用JUnit规则或者extension扩展去管理。在close()之后再调用静态方法,会直接走真实的逻辑,也就是mock失效。参考:https://rieckpil.de/mock-java-constructors-and-their-object-creation-with-mockito/

2.1.2 方法Mock

1、有返回值方法Mock

Mockito.when(xxObj.xxxMethod()).thenReturn(xxxReturnValue);

2、void无返回值方法Mock

Mockito.doNothing().when(xxObj).xxxMethod();

3、静态方法Mock

// 有返回值静态方法
Mockito.mockStatic(xxClass.class);
Mockito.when(xxClass.xxxMethod()).thenReturn(xxxReturnValue);

// 无返回值静态方法
Mockito.mockStatic(xxClass.class);
Mockito.doNothing().when(xxClass.class);

// MockedStatic close示例
private MockedStatic<SpringContextUtils> springContextUtilsMockedStatic;

@InjectMocks 
private XXX xxx;

@Test
public void testInitialize() {
    try {
        this.springContextUtilsMockedStatic = Mockito.mockStatic(SpringContextUtils.class);
        Mockito.doNothing().when(SpringContextUtils.class);
        boolean result = this.xxx.method();
        Assert.assertTrue(true);
    } finally {
        if (this.springContextUtilsMockedStatic != null) {
            this.springContextUtilsMockedStatic.close();
        }
    }
}

同样需要注意上面说的:MockedStatic对象close问题。try-with-resources的方式固然好,但是很多时候手法是:在Before阶段去MockedStatic,在After阶段close。后面给出的实例子会进行展示。

2.1.3 值Mock

1、Java内置对象类型

Mockito.anyString()
Mockito.anyInt()
...
Mockito.anyMap()

2、集合

Mockito.anyList()	// 返回值为范型,具体是什么类型需要前面确定

3、自定义对象类型

Mockito.any(xxClass.class)

2.2 Assert(断言)

1、一般断言
通过直接调用或者mock之后使得目标测试类能Run起来之后,对于分支逻辑或者结果的判断就需要对其进行断言,Junit提供的断言方式有很多,例如:assertEquals, assertNotNull,但一般情况下可以把assertTrue当成万能断言(自己把断言条件写对即可)。

Assert.assertTrue(boolean condition)
Assert.assertEquals(Object expected, Object actual)
...
Assert.assertXXX(xxx)

2、异常断言

Assert.assertThrows(String message, Class<T> expectedThrowable, ThrowingRunnable runnable)

3、非异常断言

Assertions.assertDoesNotThrow(Executable executable)

3. 单元测试实例

业务代码就是现编的,因此大家不要在意其中的阿猫阿狗,主要的目的是想把上面说的一些主要基础点能被涵盖到。单元测试涉及POM依赖如下,如果想少点可以使用mockito-all(但并不清楚mockito-all是否会包含bytebuddy)。

        <!--test-->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.12.18</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.12.18</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.6.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.6.1</version>
            <scope>test</scope>
        </dependency>

3.1 业务代码

1、Dog(主逻辑单元测试类)

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;

/**
 * 模拟一个无参构造函数的类
 *
 * @author chenx
 */
public class Dog {

    public static final int FULL_COUNT_CALORIES = 100;

    private DogFoodDao dogFoodDao;
    private boolean isFull = false;
    private int maxFoodCount = 5;
    private int maxFoodCalories = 100;


    /**
     * makeSound(狗叫)
     *
     * @param soundType
     * @return
     */
    public String makeSound(int soundType) {
        return SoundUtils.getSound(soundType);
    }

    /**
     * eatFood(干饭)
     */
    public void eatFoods() {
        int caloriesCount = 0;
        List<DogFood> dogFoodList = this.findFoods();
        for (DogFood dogFood : dogFoodList) {
            int currentCalories = dogFood.getValidCalories();
            caloriesCount += currentCalories;
        }

        this.isFull = caloriesCount >= FULL_COUNT_CALORIES;
        if (Objects.isNull(this.dogFoodDao)) {
            this.dogFoodDao = new DogFoodDao();
        }

        this.dogFoodDao.batchSave(dogFoodList);
        System.out.println("The dog's full status is " + this.isFull);
    }

    /**
     * isFull(是否吃饱)
     *
     * @return
     */
    public boolean isFull() {
        return this.isFull;
    }

    /**
     * findFoods(寻找食物)
     *
     * @return
     */
    private List<DogFood> findFoods() {
        List<DogFood> dogFoods = new ArrayList<>();
        int randomFoodCount = new Random().nextInt(this.maxFoodCount) + 1;
        for (int i = 0; i < randomFoodCount; i++) {
            dogFoods.add(this.getRandomFood());
        }

        return dogFoods;
    }

    /**
     * getRandomFood(获取随机食物)
     *
     * @return
     */
    private DogFood getRandomFood() {
        return new DogFood(new Random().nextBoolean(), new Random().nextInt(this.maxFoodCalories));
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Dog dog = new Dog();
            dog.eatFoods();
            System.out.println("-----------------");
        }
    }
}

2、DogFood(一个有参构造函数的类)

import lombok.ToString;

/**
 * 一个有参构造函数的类
 *
 * @author chenx
 */
@ToString
public class DogFood {

    /**
     * 是否能吃
     */
    private boolean canBeEaten;

    /**
     * 食物卡路里值
     */
    private int calories;

    /**
     * 有参构造函数
     *
     * @param canBeEaten
     * @param calories
     */
    public DogFood(boolean canBeEaten, int calories) {
        this.canBeEaten = canBeEaten;
        this.calories = calories;
    }

    /**
     * getValidCalories
     *
     * @return
     */
    public int getValidCalories() {
        if (this.canBeEaten) {
            return 0;
        }

        return this.calories;
    }
}

3、DogFoodDao(一个主逻辑类的依赖类)

import java.util.List;

/**
 * 一个主逻辑类的依赖类
 *
 * @author chenx
 */
public class DogFoodDao {

    /**
     * batchSave
     *
     * @param dogFoodList
     */
    public void batchSave(List<DogFood> dogFoodList) {
        System.out.println("batchSave dogFoodList into DB done, dogFoodList.size():" + dogFoodList.size());
        dogFoodList.stream().forEach(item -> {
            System.out.println(item.toString());
        });
    }
}

4、SoundUtils(一个静态方法工具类)

/**
 * 一个静态方法工具类
 *
 * @author chenx
 */
public class SoundUtils {

    private SoundUtils() {

    }

    /**
     * getSound
     *
     * @param type
     * @return
     */
    public static String getSound(int type) {
        if (type <= 0) {
            throw new RuntimeException("soundType must > 0");
        }

        return "Sound" + type;
    }
}

3.2 单元测试代码

1、SoundUtilsTest

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import untest.SoundUtils;

/**
 * SoundUtilsTest
 *
 * @author chenx
 */
@RunWith(MockitoJUnitRunner.class)
public class SoundUtilsTest {

    private int validSoundType;
    private int invalidSoundType;

    @Before
    public void mockInit() {
        this.validSoundType = 1;
        this.invalidSoundType = -1;
    }

    @Test
    public void getSoundNormalTest() {
        String result = SoundUtils.getSound(this.validSoundType);

        // 断言方式有很多,例如:assertEquals, assertNotNull,但一般情况下可以把assertTrue当成万能断言(自己把断言条件写对即可)
        Assert.assertTrue(result.length() > 0);
    }

    @Test
    public void getSoundAbnormalTest() {
        // 断言抛出异常(assertThrows)
        Assert.assertThrows("soundType must > 0", RuntimeException.class, () -> SoundUtils.getSound(this.invalidSoundType));
    }
}

2、DogTest

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import untest.Dog;
import untest.DogFood;
import untest.DogFoodDao;
import untest.SoundUtils;

import static untest.Dog.FULL_COUNT_CALORIES;

/**
 * DogTest
 *
 * @author chenx
 */
@RunWith(MockitoJUnitRunner.class)
public class DogTest {

    /**
     * InjectMocks注解:创建一个mock对象实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。此外还可以用Mockito.mock去手工mock对象。
     */
    @InjectMocks
    private Dog dog;

    @Mock
    private DogFoodDao dogFoodDao;

    private static final String MOCKED_GET_SOUND_RESULT = "mocked getSound() result";
    private MockedStatic<SoundUtils> mockedStaticSoundUtils;

    @Before
    public void mockInit() {
        // 静态方法Mock示例
        this.mockedStaticSoundUtils = Mockito.mockStatic(SoundUtils.class);
        // 有返回值方法Mock示例
        Mockito.when(SoundUtils.getSound(Mockito.anyInt())).thenReturn(MOCKED_GET_SOUND_RESULT);

        // void方法Mock示例
        Mockito.doNothing().when(this.dogFoodDao).batchSave(Mockito.anyList());
    }

    @After
    public void mockDown() {
        /**
         * MockedStatic对象,它是一个作用域模拟对象,mock的静态方法仅影响创建此静态模拟的线程,并且从另一个线程使用此对象是不安全的。
         * 当调用close() 方法时,静态模拟将会释放。如果此对象从未关闭,则静态模拟将在启动线程上保持活动状态。
         * 因此,建议在try-with-resources的语句中创建此对象,或者After阶段去手工close。
         * 在close()之后再调用静态方法,会直接走真实的逻辑,也就是mock失效。
         */
        this.mockedStaticSoundUtils.close();
    }

    @Test
    public void makeSoundTest() {
        String result = this.dog.makeSound(1);

        // 由于对SoundUtils的mock保障了getSound()的返回一定为:MOCKED_GET_SOUND_RESULT,因此可以这样断言
        Assert.assertEquals(MOCKED_GET_SOUND_RESULT, result);
    }

    @Test
    public void eatFoodsTest1() {
        /**
         * 有参构造函数对象mock示例,需要用try-with-resources的原因为:
         * 返回类型是一个MockedStatic对象,它是一个作用域模拟对象。
         * mock的静态方法仅影响创建此静态模拟的线程,并且从另一个线程使用此对象是不安全的。
         * 当调用close() 方法时,静态模拟将会释放。如果此对象从未关闭,则静态模拟将在启动线程上保持活动状态。
         * 因此,建议在try-with-resources的语句中创建此对象,或者使用JUnit规则或者extension扩展去管理。
         * 在close()之后再调用静态方法,会直接走真实的逻辑,也就是mock失效。
         */
        try (MockedConstruction<DogFood> mockedDogFood = Mockito.mockConstruction(DogFood.class,
                Mockito.withSettings().useConstructor(false, 0),
                (mock, context) -> {
                    Mockito.when(mock.getValidCalories()).thenReturn(FULL_COUNT_CALORIES);
                })) {
            this.dog.eatFoods();

            // 由于对DogFood的mock保障了getValidCalories()返回为:FULL_COUNT_CALORIES,因此一次达到full条件
            Assert.assertTrue(this.dog.isFull());
        }
    }

    @Test
    public void eatFoodsTest2() {
        /**
         * void方法正常断言用最后放一句:Assert.assertTrue(true)太Low,
         * 然而Mockito.verify()使用有一定局限性,
         * 因此使用断言不抛出异常(assertDoesNotThrow)不失为一种合理的方法
         */
        Assertions.assertDoesNotThrow(() -> this.dog.eatFoods());
    }
}

4. 总结

大家把单元测试实例中的代码去本地调试和体会一遍基本上一般的单元测试就可以直接上手去写了,毕竟在这个springboot + mybatis + redis去快速铺业务的年代很少有人会有机会涉及到复杂系统面向对象编程的机会了(各种接口,抽象类,抽象的实现,设计模式的拐弯抹角),因为可以直接拿来使用的各种缓存/存储中间件,各种RPC,各种序列化,各种框架或工具类等太多了。如果你真有机会去写一个复杂类的单元测试:里面各种依赖,各种嵌套和封装,要使其能跑起来,需要费劲各种心机去各种mock。那么你的学习能力一定能够快速的将单元测试的其他方面和技巧快速掌握了,所谓师傅领进门,修行在个人,希望大家能优雅编码,简约单元测试。

5. 补充

5.1 如何Mock非公开成员

有时候,有些非公开的成员需要mock,那么好像无路可走,可能有些同学会将被测试的代码进行修改以支持单元测试中需要的mock,例如直接public成员,或者增加public的set方法,其实还是有招去处理这种情况的,那么就是使用ReflectionTestUtils。

import org.springframework.test.util.ReflectionTestUtils;

ReflectionTestUtils.setField(Object targetObject, String name, @Nullable Object value)

下面给出一个实例:mock 非公开成员:queue、eventTranslator、ringBuffer
被测试代码:

/**
 * MailBox
 *
 * @author chenx
 */
@Slf4j
public abstract class MailBox {

    protected Disruptor<MessageEvent> queue;
    protected EventTranslatorOneArg<MessageEvent, RoutableMessage<Object>> eventTranslator;
    protected RingBuffer<MessageEvent> ringBuffer;

    protected MailBox(int capacity) {
        this.queue = new Disruptor<>(
                new MessageEventFactory(),
                getMailBoxBufferSize(capacity),
                ThreadPoolUtils.getThreadFactory("mailBoxQueueThread", null),
                ProducerType.MULTI,
                new YieldingWaitStrategy());
        this.queue.handleEventsWithWorkerPool(new MessageEventHandler());
        this.eventTranslator = new MessageEventTranslator();
    }

    /**
     * start
     */
    public void start() {
        this.ringBuffer = this.queue.start();
        if (this.ringBuffer == null) {
            throw new ChatbotException("MailBox.start() error!");
        }
    }

    /**
     * stop
     */
    public void stop() {
        try {
            this.queue.shutdown();
            this.onStop();
        } catch (Exception e) {
            log.error("MailBox.stop() error!", e);
        }
    }

    /**
     * onMessageReceived
     *
     * @param routableMsg
     */
    public abstract void onMessageReceived(RoutableMessage<?> routableMsg);

    /**
     * onStop
     */
    public abstract void onStop();

    /**
     * put
     *
     * @param routableMsg
     */
    public void put(RoutableMessage<?> routableMsg) {
        try {
            this.ringBuffer.publishEvent(this.eventTranslator, (RoutableMessage<Object>) routableMsg);
        } catch (Exception ex) {
            log.error("MailBox.put() error!", ex);
        }
    }

    /**
     * getMailBoxBufferSize: Ensure that ringBufferSize must be a power of 2
     */
    private static int getMailBoxBufferSize(int num) {
        int size = 2;
        while (size < num) {
            size <<= 1;
        }

        return size < 1024 ? 1024 : size;
    }

    /**
     * MessageEvent
     */
    public class MessageEvent {

        private RoutableMessage<Object> message;

        public RoutableMessage<Object> getMessage() {
            return this.message;
        }

        public void setMessage(RoutableMessage<Object> message) {
            this.message = message;
        }
    }

    /**
     * MessageEventFactory
     */
    public class MessageEventFactory implements EventFactory<MessageEvent> {

        @Override
        public MessageEvent newInstance() {
            return new MessageEvent();
        }
    }

    /**
     * MessageEventTranslator
     */
    public class MessageEventTranslator implements EventTranslatorOneArg<MessageEvent, RoutableMessage<Object>> {

        @Override
        public void translateTo(MessageEvent messageEvent, long l, RoutableMessage<Object> routableMessage) {
            messageEvent.setMessage(routableMessage);
        }
    }

    /**
     * MessageEventHandler
     */
    public class MessageEventHandler implements WorkHandler<MessageEvent> {

        @Override
        public void onEvent(MessageEvent messageEvent) {
            MailBox.this.onMessageReceived(messageEvent.getMessage());
        }
    }
}

单元测试代码:

/**
 * MailBoxTest
 *
 * @author chenx
 */
@RunWith(MockitoJUnitRunner.class)
public class MailBoxTest {

    @Mock
    private Disruptor<MailBox.MessageEvent> queueMock;

    @Mock
    private RingBuffer<MailBox.MessageEvent> ringBufferMock;

    @Mock
    private MailBox.MessageEventTranslator eventTranslatorMock;

    @Mock
    private RoutableMessage routableMessage;

    @InjectMocks
    private MailBox mailBox = new MailBox(100) {

        @Override
        public void onMessageReceived(RoutableMessage<?> routableMsg) {
            Assert.assertNotNull(routableMsg);
        }

        @Override
        public void onStop() {
            // do nothing
        }
    };

    @Before
    public void mockInit() {
        ReflectionTestUtils.setField(this.mailBox, "queue", this.queueMock);
        ReflectionTestUtils.setField(this.mailBox, "eventTranslator", this.eventTranslatorMock);
        ReflectionTestUtils.setField(this.mailBox, "ringBuffer", this.ringBufferMock);
    }

    @Test
    public void testStart() {
        Mockito.when(this.queueMock.start()).thenReturn(null);
        Assertions.assertThrows(ChatbotException.class, () -> this.mailBox.start());
    }

    @Test
    public void testStop() {
        this.mailBox.stop();
        Mockito.verify(this.queueMock).shutdown();
    }

    @Test
    public void testPut() {
        this.mailBox.put(this.routableMessage);
        Mockito.verify(this.ringBufferMock).publishEvent(this.eventTranslatorMock, this.routableMessage);
    }
}

原创不易,请给作者打赏或点赞,您的支持是我坚持原创和分享的最大动力!
在这里插入图片描述

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值