引入
Junit5用于构建单元化测试能力,maven依赖如下:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>xxx</version>
<scope>test</scope>
</dependency>
常用注解
@BeforAll、@AfterAll
@BeforeAll:当前的方法需要在当前类下所有用例执行之前执行一次,且被该注解修饰的方法必须为静态方法。
@AfterAll:当前的方法需要在当前类下所有用例执行之后执行一次,且被该注解修饰的方法必须为静态方法。
需要注意:这个方法配合MockStatic使用,会使mock对象在测试类间互相影响。
@BeforEach、@AfterEach
@BeforeEach:当前的方法需要在每个用例执行之前都执行一次
@AfterEach:当前的方法需要在每个用例执行之后都执行一次
@Disabled
表示忽略,被这个注解的测试方法不被执行
@ExtendWith
提供扩展相关能力
参数化
参数化就是尽可能的通过一个用例,多组参数来模拟用户的行为;在使用参数化注解之前需要先用 @parameterizedTest
声明该方法为参数化方法,然后再通过注解提供数据来源。需集成param包。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
</dependency>
单参数
使用@ValueSource获取参数
@ValueSource(数据类型方法={参数1,参数2…})
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void Test01(int num) {
System.out.println(num);
}
从csv获取参数
@CsvFileSource(resources = "____.csv")
这个时候我们需要在 main 文件下 resources 中创建一个 test.csv 文件(.csv 和 参数必须相同):
@ParameterizedTest
@CsvFileSource(resources = "test.csv")
void Test03(String name) {
System.out.println(name);
}
从方法获取参数
@CsvFileSource
、@ValueSource
只能传递同种类型的参数,那么我们想要传多种参数,那么可以用方法获取参数。@MethodSource("Generator")
public static Stream<Arguments> Generator() {
return Stream.of(Arguments.arguments(1, "张三"),
Arguments.arguments(2, "李四"),
Arguments.arguments(3, "王五")
);
}
@ParameterizedTest
@MethodSource("Generator")
void Test04(int num, String name) {
System.out.println(num + ":" + name);
}
多参数
多参数:@CsvSource({“数据组合1”,“数据组合2”…}),每个双引号是一组参数(测试用例)
//多参数:@CsvSource({“数据组合1”,“数据组合2”…})
@ParameterizedTest
@CsvSource({"1, 张三", "2, 李四", "3, 王五"})
void manyTest(int num, String name) {
System.out.println("num:" + num + ", name:" + name);
}
用例执行顺序
顺序执行:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
,然后再每次执行的时候添加@order(int)
。随机执行:
@TestMethodOrder(MethodOrderer.class)
Assertions - 断言
org.junit.jupiter.api.Assertions
提供断言能力,常用包括:
断言匹配/不匹配:
assertEquals()
、assertNotEquals()
断言结果为真/为假:
assertTrue()
、assertFalse()
断言结果为空/非空:
assertNull()
、assertNotNull()
断言抛出异常:
assertThrows()
测试套件
Mockito与打桩
提供成员方法和静态方法的mock能力,它的作用就是模拟一个可以满足测试的假想对象
maven依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${org.mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${org.mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${org.mockito.version}</version>
<scope>test</scope>
</dependency>
设置测试类依托mockito管理
Mock 需要先设置测试类被 Mockito 管理,有两种方式:
在spring boot项目中,必须在测试类上添加注解Junit5以下使用
@RunWith(MockitoJUnitRunner.class)
,Junit5使用@ExtendWith(MockitoExtension.class)
@RunWith(MockitoJUnitRunner.Silent.class)
标签可以用于排除一些cleanCode错误时使用在
@Before
方法中,调用MockitoAnnotations.initMocks(this)
mock类替身对象技术
对类构造替身对象
对类构造替身对象有如下几种方式:
在测试用例中直接使用
Mockito.mock(Class)
方法,生成替身对象在测试类中声明需生成替身的依赖类,使用
@Mock
注解。将替身注入到被测试类。注入可以使用
@InjectMocks
注解标识需要被注入的类。依赖的其他类 就用@Mock
注解标识,Mockito 自动将替身注入到被测试类。
第二种方式是该测试类中所有单元测试唯一的对象,为了防止每个单元测试 stubbing 的先后顺序对其他单元测试的影响,最好写一个 @After 方法,在该方法中使用 Mockito.reset() 方法清空 stubbing 规则
stubbing - 模拟替身对象的行为
最常用的一种方式,就是设置某种方法根据某个入参的返回值是什么,或者设置抛出什么异常:
设置返回对象方式一:
when().thenReturn()
;
Mockito.when(mock.action(Mockito.anyMap())).thenReturn(mock);
设置返回对象方式二:
doReturn().when(object).method()
;设置抛出异常:
when().thenThrow()
;
void方法什么都不做执行空逻辑:Mockito.doNothing().when(对象).方法
mock多次返回
@Test
public void testMockReturnMultiple2() {
Mockito.when(list.size()).thenReturn(1).thenReturn(2).thenReturn(3).thenReturn(4);
assertThat(list.size(), equalTo(1));
assertThat(list.size(), equalTo(2));
assertThat(list.size(), equalTo(3));
assertThat(list.size(), equalTo(4));
}
mock类的替身对象案例
@ExtendWith(MockitoExtension.class)
public class SchoolTest {
// 使用第二种方式
@Mock
Student student;
@InjectMocks
SchoolClass schoolClass;
@BeforeEach
public void setUp() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.openMocks(SchoolTest.class);
}
@Test
public void test1() {
// 使用第一种方式
Teacher teacher = Mockito.mock(Teacher.class);
Mockito.when(teacher.teach(student)).thenReturn("I'm teaching");
Mockito.doReturn(“XiaoMing”).when(student).getNameByNo(any());
List<String> nameList = schoolClass.listNameByClassNo(“No1”);
Assertions.assertEquals(10, nameList.size());
}
}
mock静态方法技术
private MockedStatic<StaticClass> beanFactoryMockedStatic;
beanFactoryMockedStatic = Mockito.mockStatic(StaticClass.class);
beanFactoryMockedStatic.when(() -> StaticClass.staticMethod(入参)).thenReturn(返回值);
可以用any()代表传入任何参数
静态类mock完后必须要关闭,否则会持续留在内存中影响后续测试
@AfterEach
public void after() {
beanFactoryMockedStatic.close();
}
通用参数匹配增强
Matchers.any()
:匹配任意对象,带着 any 名称的方法 还有 anyInt,anyObject 等都是一样的作用。Matchers.eq()
:when(list.get(eq(0)) 跟 when(list.get(0) 没区别。提供这个主要是可以在通用参数匹配中,指定特殊的具体value匹配。Matchers.isA()
:表示匹配参数为任意某个类及其子类的对象。anyBoolean()
:用于匹配布尔类型,anyMap()用于匹配任意map
值得注意的事使用通用匹配的话 需要都用通用匹配函数,如果想指定某个参数是特殊值 用eq()
包装一下
案例:
@ExtendWith(MockitoExtension.class)
public class SchoolTest {
@Mock
Student student;
@InjectMocks
SchoolClass schoolClass;
@BeforeEach
public void setUp() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.openMocks(SchoolTest.class);
}
@Test
public void test1() {
Mockito.doReturn(“XiaoMing”).when(student).getNameByNo(any());
List<String> nameList = schoolClass.listNameByClassNo(“No1”);
Assertions.assertEquals(10, nameList.size());
}
}
上面的案例中,假设SchoolClass中有内部类Student,SchoolClass有方法listNameByClassNo
,要测试这个方法的逻辑,内部依赖school对象调用getNameByNo
方法,因此mock内部类Student对象,并注入SchoolClass对象中,同时对student的getNameByNo
方法进行方法mock。
更优雅的断言 - HamcrestMatchers
equalTo()
实际值等于期望对象not()
不如何如何 可以组装 equalTo() 例如 not(equalTo()) 表示实际值不等于期望对象is()
跟equalTo()
一样greaterThan()
实际值大于期望对象lessThan()
实际值小于期望对象either().or()
或关系,满足其一即可both().and()
且关系,两个条件都得满足anyOf()
满足其中之一即可,相当于INallOf()
所有都满足 相当于一堆 AND
跨线程mock技术
Mockito进行跨线程mock存在难点,其原理是:MockStatic是通过一个Map管理的,每个线程构造一个对象,负责的管理自己的Map,因此在主线程Mock好的,子线程感知不到
基于这个原理,有两种跨线程mock的方案:
mock线程池,让其中的线程在执行前首先进行MockStatic,再执行原有方法
直接获取到子线程,将主线程的ThreadLocal中的mockMap传递给子线程
假设有一个mock,需要,mock在主线程中执行,但是方法执行的时候是在new ThreadPoolExecutor().submit()
中:
public static void mockStart() {
MockedStatic<ServiceSwitchUtil> ServiceSwitchUtilMock = Mockito.mockStatic(ServiceSwitchUtil.class);
ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));
}
方案一
首先提供一个mock线程池的方法,思路就是对任意线程返回一个指定的Answer
public static ThreadPoolExecutor mockExecutor(Answer<Runnable> answer) {
ThreadPoolExecutor mockExecutor = Mockito.mock(ThreadPoolExecutor.class);
Mockito.lenient().doAnswer(answer).when(mockExecutor).submit(any(Runnable.class));
Mockito.lenient().doAnswer(answer).when(mockExecutor).execute(any(Runnable.class));
return mockExecutor;
}
选择特定的线程,对线程进行mock
// 首先构造一个Answer对象,这个对象的思路是先执行mock,然后执行线程自己的runnable方法
Answer<Runnable> answer = (invocation) -> {
// 先取出这个线程原本的runnable可执行对象
final Runnable runnable = invocation.getArgument(0);
// 构造一个新的runnable
Runnable wrapperRunnable = () -> {
// 先执行mock
mockStart();
MockUtils.mockSftpFileSystem();
// 再执行线程中原本的runnable
runnable.run();
};
// 构造一个新线程并执行
Thread thread = new Thread(wrapperRunnable, "junit-mock-consumer-thread-"+count.incrementAndGet());
thread.start();
return null;
};
// 将构造的Answer作为参数mock到线程池中,并用mock好的线程池替换主线程构造线程池的时候构造的变量
ThreadPoolExecutor mockExecutor = MockUtils.mockExecutor(answer);
MockUtils.setField(consumerScheduler, "pool", mockExecutor);
方案二
启动线程,执行mock,完成新线程的mock配置,同时通过线程组获取所有线程,定义一个match方法从所有线程中找到需要mock的子线程,执行mockSubThread方法
new Thread(() -> {
// 执行mock
mockStart();
// 获取线程组
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
int count = threadGroup.activeCount();
Thread[] threads = new Thread[count];
threadGroup.enumerate(threads);
for (Thread thread : threads) {
if (match(thread)) {
mockSubThread(thread)
}
}
完成ThreadLocal的传递,这里先在当前线程中取出了InlineByteBuddyMockMaker中配置好的mapMap,然后通过pushTo方法把它推送到了子线程中,从而完成了ThreadLocal的传递
Field field = MockUtil.class.getDeclaredField("mockMaker");
field.setAccessible(true);
InlineByteBuddyMockMaker inlineByteBuddyMockMaker = (InlineByteBuddyMockMaker) field.get(MockUtil.class);
Field inlineDelegateByteBuddyMockMaker = inlineByteBuddyMockMaker.getClass().getDeclaredField("inlineDelegateByteBuddyMockMaker");
inlineDelegateByteBuddyMockMaker.setAccessible(true);
InlineDelegateByteBuddyMockMaker inlineDelegateByteBuddyMockMaker2 = (InlineDelegateByteBuddyMockMaker) inlineDelegateByteBuddyMockMaker.get(inlin
Mockito源码分析
成员方法mock源码思路分析
……
静态类mock源码思路分析
MockedStatic<ServiceSwitchUtil> ServiceSwitchUtilMock = Mockito.mockStatic(ServiceSwitchUtil.class);
ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));
通过这段示例代码,对一个服务的开关工具类进行了Mock,从而实现调用ServiceSwitchUtil::getSwitches
方法,获得一个自定义的开关列表
分析这段逻辑,首先是构建Mock的Mockito#mockStatic
方法,然后是.when().thenReturn()
方法
Mockito
核心成员变量
// mockitoCore,执行Mock的真正核心代码
static final MockitoCore MOCKITO_CORE = new MockitoCore();
// 如果 mock 没有存根,则每个 mock 的默认值Answer。通常它只返回一些空值
public static final Answer<Object> RETURNS_DEFAULTS = Answers.RETURNS_DEFAULTS;
mockStatic
return mockStatic(classToMock, withSettings());
简单看下withSettings()
方法
return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
其实是给定一个默认返回
关键内容在下层的mockStatic()
return MOCKITO_CORE.mockStatic(classToMock, mockSettings);
委托给MockitoCore#mockStatic
执行
MockitoCore
mockStatic - 核心骨架方法
MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
// 构造一些mockStatic的默认配置,包括mock名等
MockCreationSettings<T> creationSettings = impl.buildStatic(classToMock);
// 构造静态mock
MockMaker.StaticMockControl<T> control = createStaticMock(classToMock, creationSettings);
// 向构造的静态mock中添加代理执行器
control.enable();
mockingProgress().mockingStarted(classToMock, creationSettings);
// 封装返回
return new MockedStaticImpl<>(control);
静态mock的核心框架方法,经历以下几个核心步骤,补充跳转:
其中MockSettings
的实现类 MockSettingsImpl
和 CreationSettings
承载着相关配置信息,比如要为待 mock 的对象生成怎样的返回值
MockUtil
核心成员变量
// mock构造器
private static final MockMaker mockMaker = Plugins.getMockMaker();
跟进下看看MockMaker取的是哪里,找到PluginRegistry#mockMaker
private final MockMaker mockMaker =
new PluginLoader(
pluginSwitch,
DefaultMockitoPlugins.INLINE_ALIAS,
DefaultMockitoPlugins.PROXY_ALIAS)
.loadPlugin(MockMaker.class);
这里可以看到默认的Plugin是一个map存放的
DEFAULT_PLUGINS.put(
INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker");
INLINE_ALIAS对应的是InlineByteBuddyMockMaker这个类
createStaticMock
MockHandler<T> handler = createMockHandler(settings);
return mockMaker.createStaticMock(type, settings, handler);
这里是两个步骤,首先构造MockHandler(Mock对象的代理),然后通过MockMaker启动静态mock
这里MockMaker
createMockHandler
createMockHandler是通过MockHandlerFactory方法返回handler对象
MockHandler<T> handler = new MockHandlerImpl<T>(settings);
MockHandler<T> nullResultGuardian = new NullResultGuardian<T>(handler);
return new InvocationNotifierHandler<T>(nullResultGuardian, settings);
MockHandler
接口为Mock类提供代理执行的能力,InvocationNotifierHandler
是MockHandler接口实现类的封装
MockHandlerImpl - mock代理对象核心
handle
handle方法是匹配mock和方法并执行的核心,比较复杂
if (invocationContainer.hasAnswersForStubbing()) {
// stubbing voids with doThrow() or doAnswer() style
InvocationMatcher invocationMatcher =
matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(), invocation);
invocationContainer.setMethodForStubbing(invocationMatcher);
return null;
}
首先判断如果stubbing是doAnswer或doThrow两种,优先执行
// 获取验证模式
VerificationMode verificationMode = mockingProgress().pullVerificationMode();
// 获取调用匹配器对象
InvocationMatcher invocationMatcher =
matchersBinder.bindMatchers(
mockingProgress().getArgumentMatcherStorage(), invocation);
mockingProgress().validateState();
// if verificationMode is not null then someone is doing verify()
if (verificationMode != null) {
……
}
这一部分如果需要verify,在if中执行校验方法
// prepare invocation for stubbing
invocationContainer.setInvocationForPotentialStubbing(invocationMatcher);
OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainer);
mockingProgress().reportOngoingStubbing(ongoingStubbing);
构造OngoingStubbingImpl对象,这里可以看到handle每次调用都会创建新的对象存在mockingProgress上下文中,取用流程可以参考后面MockedStaticImpl#when方法
StubbedInvocationMatcher stubbing = invocationContainer.findAnswerFor(invocation);
……
if (stubbing != null) {
……
try {
return stubbing.answer(invocation);
} else {
// 如果打桩对象为空,也就是mock对象的默认行为,由于mock对象的每一个操作都是“无效”的,因此都会返回一个相应类型的默认值(stub除外)
Object ret = mockSettings.getDefaultAnswer().answer(invocation);
DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
return ret;
}
为当前拦截的调用对象查找存在的返回对象answer,执行返回对应的stubbing,如果大壮对象为空,则返回默认值
InlineDelegateByteBuddyMockMaker - MockMaker接口实现类
核心成员变量
private final DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics =
new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE);
Mockito包下定义的一个ThreadLocal,标志着mock是线程唯一的,这里导致了MockStatic不能在一个线程中多次执行
createStaticMock
if (type == ConcurrentHashMap.class) {
throw new MockitoException(
……
} else if (type == Thread.class
|| type == System.class
|| type == Arrays.class
|| ClassLoader.class.isAssignableFrom(type)) {
throw new MockitoException(
……
}
首先可以看到对于ConcurrentHashMap的Mock、对Thread、System、Arrays的mock会报错
bytecodeGenerator.mockClassStatic(type);
基于ByteCodeGenerator生成代理
Map<Class<?>, MockMethodInterceptor> interceptors = mockedStatics.get();
if (interceptors == null) {
interceptors = new WeakHashMap<>();
mockedStatics.set(interceptors);
}
可以看到构造静态mock实际上是向DetachedThreadLocal中设置值
return new InlineStaticMockControl<>(type, interceptors, settings, handler);
构造一个封装了mock代理类的封装并返回,这里可以看到,每个线程返回各自的封装类,存放各自的Mock库,因此决定了名每个mock静态类只能mock一次
InlineStaticMockControl - 静态mock封装
构造函数
private InlineStaticMockControl(
Class<T> type,
Map<Class<?>, MockMethodInterceptor> interceptors,
MockCreationSettings<T> settings,
MockHandler handler) {
this.type = type;
this.interceptors = interceptors;
this.settings = settings;
this.handler = handler;
}
用interceptors变量存储传入的map;用handler对象承接传入的
enable - 激活mock
if (interceptors.putIfAbsent(type, new MockMethodInterceptor(handler, settings))
!= null) {
throw new MockitoException(
join(
"For "
+ type.getName()
+ ", static mocking is already registered in the current thread",
"",
"To create a new mock, the existing static mock registration must be deregistered"));
}
可以看到,如果一个线程多次mock一个静态类,即这里多次执行putIfAbsent,会导致抛出异常,同时输出这段日志
disable - 注销mock
if (interceptors.remove(type) == null) {
throw new MockitoException(
join(
"Could not deregister "
+ type.getName()
+ " as a static mock since it is not currently registered",
"",
"To register a static mock, use Mockito.mockStatic("
+ type.getSimpleName()
+ ".class)"));
}
对于在一个线程中用完的静态类,执行close方法,最终调用到这里,实际执行的是map的remove方法
MockStatic模拟stubbing源码分析
ServiceSwitchUtilMock.when(ServiceSwitchUtil::getSwitches).thenReturn(ImmutableMap.of(……));
回顾上面的案例,一个静态mockstubbing的执行实际上就是.when().thenReturn()的流程
MockedStaticImpl
when
// 初始化MockProgressImpl对象
MockingProgress mockingProgress = mockingProgress();
// 标记stub开始
mockingProgress.stubbingStarted();
// 获取最新的ongoingStubbing对象
@SuppressWarnings("unchecked")
OngoingStubbing<S> stubbing = (OngoingStubbing<S>) mockingProgress.pullOngoingStubbing();
if (stubbing == null) {
mockingProgress.reset();
throw missingMethodInvocation();
}
// 返回ongoingStubbing对象
return stubbing;
其中OngoingStubbing对象会在每次MockHandlerImpl调用handle方法时创建一个,然后set到ThreadLocal的mockingProgress中,所以这里取出来的就是上一次的调用,这里也证明了其实when的参数是没用的,只要mock对象有方法调用就可以了。因此,when方法就是返回上次mock方法调用封装好的OngoingStubbing
BaseStubbing
thenReturn
通过链式展示thenRetrun方法中调用的顺序
// OngoingStubbing#thenReturn
@Override
public OngoingStubbing<T> thenReturn(T value) {
return thenAnswer(new Returns(value));
}
封装Returns,向下调用
OngoingStubbingImpl#thenAnswer
@Override
public OngoingStubbing<T> thenAnswer(Answer<?> answer) {
if(!invocationContainer.hasInvocationForPotentialStubbing()) {
throw incorrectUseOfApi();
}
//把这个answer加入到invocationContainer对象中保存
invocationContainer.addAnswer(answer);
return new ConsecutiveStubbing<T>(invocationContainer);
}
通过invocationContainer保存answer
InvocationContainerImpl#addAnswer
public StubbedInvocationMatcher addAnswer(Answer answer, boolean isConsecutive) {
// 1.获取stub的调用
Invocation invocation = invocationForStubbing.getInvocation();
// 2.标记stub完成
mockingProgress().stubbingCompleted();
if (answer instanceof ValidableAnswer) {
((ValidableAnswer) answer).validateFor(invocation);
}
// 3.把stub的调用和answer加到StubbedInvocationMatcher的list
// 这里的意思就是把调用和返回绑定,如果下次调用匹配到了,就返回对应的answer
synchronized (stubbed) {
if (isConsecutive) {
stubbed.getFirst().addAnswer(answer);
} else {
stubbed.addFirst(new StubbedInvocationMatcher(invocationForStubbing, answer));
}
return stubbed.getFirst();
}
}
完成answer的添加,这里在InvocationContainerImpl中可以看出来stubbed是一个LinkedList
private final LinkedList<StubbedInvocationMatcher> stubbed = new LinkedList<>();
评论区