赞
踩
我个人认为对于是否要写单元测试这件事情走极端都是不对的,即,【支持方】强制要求必须要写单元测试,强制要求代码单元测试代码覆盖率XX%以上。【反对方】花了甚至比写代码还多的时间去写单元测试然后卵用没有(springboot+mybatis+redis堆业务那代码可不写的快吗?然后由于之前没有写过单元测试,因此导致花的时间更多)。
对于单元测试,个人认为正确的态度是:不强制要求覆盖率一定达到多少,也不认同完全不写单元测试。毕竟通过写单元测试至少可以获得如下收益或者便利:
本文通过一个自己现编的Dog对象的单元测试实例,将Mockito+Junit单元测试的一些常用知识点和方法的使用进行演示,不过实例展示之前还是有必要先讲讲单元测试的基础知识点。
mock这个词硬要去翻译的话好像也找不到合适的词,因此简单用模拟去表达。因为在单元测试的编写过程中,一个最基本的要求是单元测试本身的纯粹性,即我只想测试某一个类或者某一个方法,然而多数的时候情况往往不单纯,例如,目标测试类中依赖了很多其他类及这些类的方法及这些方法的运行结果(要使得测试目标类能跑起来,有时需要mock这些依赖)。另外,单元测试还会要求尽快跑完(要求严格的公司,会在每次build之前跑一次全量单元测试),不因为单元测试而产生真实的数据等(mock dao层对象和其方法即可)。基于这些我们一定要去mock,可以说写单元测试一定要会mock,并且往往是一顿mock猛如虎。
1、无参构造函数对象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/
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。后面给出的实例子会进行展示。
1、Java内置对象类型
Mockito.anyString()
Mockito.anyInt()
...
Mockito.anyMap()
2、集合
Mockito.anyList() // 返回值为范型,具体是什么类型需要前面确定
3、自定义对象类型
Mockito.any(xxClass.class)
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)
业务代码就是现编的,因此大家不要在意其中的阿猫阿狗,主要的目的是想把上面说的一些主要基础点能被涵盖到。单元测试涉及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>
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;
}
}
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());
}
}
大家把单元测试实例中的代码去本地调试和体会一遍基本上一般的单元测试就可以直接上手去写了,毕竟在这个springboot + mybatis + redis去快速铺业务的年代很少有人会有机会涉及到复杂系统面向对象编程的机会了(各种接口,抽象类,抽象的实现,设计模式的拐弯抹角),因为可以直接拿来使用的各种缓存/存储中间件,各种RPC,各种序列化,各种框架或工具类等太多了。如果你真有机会去写一个复杂类的单元测试:里面各种依赖,各种嵌套和封装,要使其能跑起来,需要费劲各种心机去各种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);
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。