单元测试之mockito
简介
mockito是一款模拟测试框架,用于Java开发中的单元测试。通过mockito,可以创建和配置一个对象,通过它来替换对象的外部依赖。
作用:模拟一个类的外部依赖,保证单元测试的独立性。例如,在类A中会调用类B提供的功能,那么类A就依赖于类B,这个时候,为类A编写的单元测试,依赖于类B提供的功能,但是类B可能是不稳定的,它可能是一个rpc接口、或者是一个dao接口,rpc接口可能会出现网络问题,数据库中的数据可能会被别人修改,所以,就使用mockito来模拟类B,将模拟出的实例注入到类A的实例中,此时,在为类A编写的单元测试中,它依赖的模拟出的类B,它不再受具体外部环境的干扰,无论执行多少次都可以获得相同的结果。通过mockito,保证了单元测试的独立性,这是回归测试的基础,同时也是测试驱动开发的基本技术。
回归测试:指修改了旧代码后,重新执行以前的测试,以确认修改没有引入新的错误或导致其它代码产生错误
测试驱动开发:Test-Driven Development,TDD,在开发功能代码之前,先编写单元测试,测试代码明确需要编写什么产品代码,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。
一个优秀的单元测试应该具备的特点:
- 一个测试不应该依赖于外部环境
- 一个单元测试不依赖与另一个单元测试的结果,单元测试之间的执行顺序不会改变单元测试的结果
- 单元测试的结尾必须是断言
入门案例
在这个入门案例中,模拟mockito在实际开发中的使用场景,演示mockito在单元测试中究竟起到了什么作用,这也是我学习mockito之前最困惑的一点。
环境准备
第一步:编辑pom文件,添加junit、mockito、servlet、lombok依赖
<!--junit-->
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope>
</dependency><!--mockito -->
<dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>2.23.4</version><scope>test</scope>
</dependency><!--servlet依赖-->
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.0.1</version><scope>provided</scope>
</dependency><!--lombok-->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>compile</scope>
</dependency>
第二步:编写实体类Account
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {private String username;private String password;
}
第三步:编写mapper类
public class AccountMapper {public Account findAccount(String username, String password) {return new Account("aa", "12345");}public Boolean existsAccount(Account account) {return false;}
}
第四步:编写控制器
/*** 模拟一个常见的控制器*/
public class AccountController {private final AccountMapper mapper;public AccountController(AccountMapper mapper) { this.mapper = mapper; }public String login(HttpServletRequest request) {final String username = request.getParameter("username");final String password = request.getParameter("password");try {Account account = mapper.findAccount(username, password);if(account == null) {return "/login";} else {return "/index";}} catch (Exception e) {Utils.println("登录出现异常");e.printStackTrace();return "/505";}}
}
需求
需求:编写AccountController的单元测试
上述代码是在模拟一个实际的项目,当然实际开发会比这更加复杂,但是用于了解mockito的功能是比较合适的。
在上述代码中,AccountController依赖于AccountMapper,假设AccountMapper是一个rpc接口,需要在指定的环境中调用,但是一个好的单元测试不应该依赖于外部环境,它最好可以重复执行,此时,需要使用mockito来模拟AccountMapper,使开发者可以专注于测试AccountController中的功能。
完成需求
编写测试案例,使用mockito,模拟外部依赖
案例1:使用mock方法来创建AccountMapper的模拟对象,同时在模拟对象上进行方法打桩,设置模拟对象的行为
@Test
public void test2(){// 创建AccountMapper的模拟对象AccountMapper mapper = Mockito.mock(AccountMapper.class);// 方法打桩:设置模拟对象的行为Mockito.when(mapper.findAccount("aa", "123")).thenReturn(null);// 执行被测试类中的方法AccountController controller = new AccountController(mapper);String loginResult = controller.login("aa", "123");// 断言:设置mapper.findAccount()方法的返回值为null,代表登录失败,返回登录页面Assert.assertEquals("/login", loginResult);
}
案例2:同案例1一样,只不过这次设置模拟对象的方法抛出异常
@Test
public void test3(){AccountMapper mapper = Mockito.mock(AccountMapper.class);// 方法打桩:抛出异常Mockito.when(mapper.findAccount("aa", "123")).thenThrow(new RuntimeException());AccountController controller = new AccountController(mapper);String loginResult = controller.login("aa", "123");// 断言:被测试方法返回'/505'Assert.assertEquals("/505", loginResult);
}
入门案例讲解
模拟对象:使用mock方法创建AccountMapper的模拟对象,将它注入到AccountController中,此时,AccountController的依赖被替换为模拟对象,它不再依赖于具体的环境,也就是真实的AccountMapper实例。
方法打桩:使用when方法、thenReturn方法、thenThrows方法,来设计模拟对象的行为。
概念和特性
模拟对象:mockito可以创建模拟对象,代替真实的对象作为被测试类的依赖,这样可以在测试中完全控制这些对象的行为和返回值。
方法打桩:通过方法打桩设置预期行为,用户可以定义模拟对象在接收到特定方法调用时应如何响应,比如返回特定值或抛出异常。
监视:mockito可以监视真实的对象或模拟对象上的方法调用,用于随后验证。
验证:在测试结束后检查模拟对象是否如预期那样被调用了正确的方法和次数。
基本使用
在之前的案例中,学习了mockito的使用场景,和基本的使用方法。我一开始接触mockito的时候,最困惑的就是它的使用场景,我不明白为什么要把单元测试搞得这么复杂,学完之后才解开了自己的困惑,所以在这里我把使用场景放在最开头,接下来详细地了解mockito中的各项功能。
创建模拟对象
调用mock方法,创建模拟对象
@Test
public void test1() {// 创建一个mock对象List mockList = Mockito.mock(List.class);// 判断mock对象的类型assert mockList instanceof List;
}
方法打桩:设置方法正常返回
配置调用模拟对象的某个方法时的返回值
@Test
public void test2() {List mockList = Mockito.mock(List.class);// 方法打桩:配置模拟对象上某个方法的行为,这里配置add("one")时返回trueMockito.when(mockList.add("one")).thenReturn(true);assert mockList.add("one"); // trueassert !mockList.add("two"); // false// 方法打桩,配置模拟对象调用size()方法时返回1Mockito.when(mockList.size()).thenReturn(1);assert mockList.size() == 1;
}
方法打桩:设置方法抛异常
配置模拟对象抛出异常
@Test
public void test3() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.remove("aa")).thenThrow(new NoSuchElementException("没有该元素"));String msg = null;try {mockList.remove("aa");} catch (NoSuchElementException e) {msg = e.getMessage();}assert msg.equals("没有该元素");
}
为返回值为void的方法进行打桩
这里需要使用doThrow方法
@Test(expected = RuntimeException.class)
public void test7() {List mockList = Mockito.mock(List.class);Mockito.doThrow(new RuntimeException("异常")).when(mockList).add(1, 1);mockList.add(1, 1);
}
检测模拟对象的方法调用
mockito会追踪模拟对象的所有方法调用和调用方法时传递的参数,使用verify方法,可以检测指定方法的调用是否符合要求
@Test
public void test4() {List mockList = Mockito.mock(List.class);mockList.add(1);mockList.add(2);mockList.add(2);Mockito.verify(mockList, Mockito.times(1)).add(1);Mockito.verify(mockList, Mockito.times(2)).add(2);Mockito.verify(mockList, Mockito.atLeastOnce()).add(1);Mockito.verify(mockList, Mockito.times(0)).isEmpty();
}
监视真实对象
调用spy方法,可以包装一个真实的对象,如果spy对象没有设置打桩,所有的方法都会调用对象实际的方法,使用这种方式,可以对于存量代码进行单测。
有些时候不想对一个对象进行mock,但是想判断一个普通对象的方法有没有被调用过,那么可以使用spy方法来监测对象,然后用verify 来验证方法有没有被调用。
@Test
public void test5() {List<String> list = new ArrayList<>();List<String> spyList = Mockito.spy(list);spyList.add("1");spyList.add("2");spyList.add("3");// 方法打桩Mockito.when(spyList.size()).thenReturn(1);assert spyList.size() == 1; // 调用打桩后的方法而不是真实的方法
}
参数匹配器
更加灵活地进行打桩和验证,例如anyInt(),代表任意大小的int值
@Test
public void test6() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.get(Mockito.anyInt())).thenReturn("aaa");assert mockList.get(0).equals("aaa");assert mockList.get(8).equals("aaa");
}
设置调用一个方法时的具体行为
thenAnswer方法,它可以设置调用一个方法时的具体行为,而不是像thenReturn一样,返回一个具体值
@Test
public void test(){AccountMapper mapper = Mockito.mock(AccountMapper.class);// 设置调用一个方法时的具体行为,在这里比较简单,知识返回一个具体的对象Mockito.when(mapper.findAccount("aa", "123")).thenAnswer(new Answer<Object>() {@Overridepublic Object answer(InvocationOnMock invocation) throws Throwable {return new Account("bb", "234");}});AccountController con = new AccountController(mapper);String loginResult = con.login("aa", "123");// 登录成功Assert.assertEquals("/index", loginResult);
}
验证方法的调用次数
public void test9() {List mockList = Mockito.mock(List.class);Mockito.when(mockList.get(0)).thenReturn("123");assert mockList.get(0).equals("123");Mockito.verify(mockList, Mockito.times(1)).get(0); // 验证指定方法被调用了一次
}
模拟静态方法
在Mockito的早期版本中,它不支持直接模拟静态方法。但是,从Mockito 3.4.0版本开始,Mockito通过扩展库mockito-inline提供了对模拟静态方法的支持。
模拟静态方法应该谨慎使用,因为静态方法通常作为全局状态或工具方法,它们的模拟可能会影响程序的其他部分。
添加依赖:
<dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>3.4.0</version><scope>test</scope>
</dependency>
<dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>3.4.0</version><scope>test</scope>
</dependency>
编写代码:
@Test
public void mulTest() {try (MockedStatic<CalculateUtils> theMock = Mockito.mockStatic(CalculateUtils.class)) {//对CalculateUtil.mul(11,22)进行mock,让其返回99Mockito.when(CalculateUtils.mul(11, 22)).thenReturn(99);//调用int result = CalculateUtils.mul(11, 22);assert result == 99;}
}
对于mockito 来说,一旦声明了一个 MockedStatic,它会一直留在当前的线程中并且会对当前线程中所有调用的代码产生影响,这个影响不仅仅局限于单元测试,甚至会对测试框架(TestNG,Junit等)产生影响,所以一定要保证在测试代码结束后对 MockedStatic 进行关闭,否则可能会对其他单元测试产生影响。在jdk1.8中,可以通过try resource语句来关闭MockedStatic
注解
常用注解:
- @Mock:相当于mock方法
- @Spy:相当于spy方法
- @InjectMocks:把被@Mock和@Spy修饰的成员变量注入到当前成员变量中
使用案例:
public class Mockito3Test {@InjectMocks@Spy //加上@Spy,表示监视真实的对象,同时防止mock多线程运行报错private AccountController controller;@Mockprivate AccountMapper mapper;@Beforepublic void before() {MockitoAnnotations.initMocks(this); // 使用注解前,创建环境,使注释生效}@Testpublic void test() {String login = controller.login("aa", "123");// 模拟登录失败Mockito.when(mapper.findAccount("aa", "123")).thenReturn(null);Assert.assertEquals("/login", login); // /返回login表示登录失败}
}
总结
在使用mockito这一章节中,总结了mockito的常用功能,同时结合在入门案例中提到了mockito的使用场景,来学习mockito的常见用法。
使用mock方法,来创建一个模拟对象,将模拟对象注入到待测试的实例中,此时,待测试的实例依赖的是模拟对象而不是真实对象,通过模拟对象,可以获得稳定的外部依赖,保证单元测试可以重复执行。
使用spy方法,监视一个真实的对象,随后可以调用verify方法来验证对于真个真实对象的调用情况。
mock方法和spy方法的区别在于,mock方法接收一个类对象作为参数,根据这个类对象创建一个模拟对象,spy方法接收一个实例作为参数,它会监视这个实例。
常用API总结
在之前的章节中学习了mockito的使用场景和具体功能,其中涉及到了很多api,在这里记录一下这些api的基本功能
org.mockito
Mockito:public class Mockito extends ArgumentMatchers:提供了mockito的核心功能
-
mock方法:
public static <T> T mock(Class<T> classToMock)
:使用参数指定的类对象创建一个mock对象。具体方式是,在内存中动态地创建一个类,这个类是参数指定的类的子类,然后创建这个类的实例,这个实例就是mock对象,由它来完成方法打桩、调用统计等功能。 -
when方法:
public static <T> OngoingStubbing<T> when(T methodCall)
:方法打桩,打桩是指设置方法在指定情况下的行为,例如,传入一个参数,返回一个结果,这种设置并不会改变方法本身的行为,它的作用是在模拟的对象上设置方法的行为,方便测试,类似于造数据。 -
spy方法:
public static <T> T spy(Class<T> classToSpy)
:使用spy方法模拟出的对象,会实际调用类中的方法,除非用户设置了方法打桩。参数可以传入一个类对象或一个实际的对象 -
verify方法:
public static <T> T verify(T mock)
:验证某些之前发生过一次的行为,如果这些行为发生过,没有问题,如果没有发生过,报错。验证方法行为的案例:
List<Object> list = mock(List.class);
list.add("aa");
list.add("bb");
verify(list).add("aa"); // 不报错
verify(list).add("cc"); // 报错
- reset:
public static <T> void reset(T... mocks)
:重置,之前在这个对象上的打桩方法全部消除 - anyInt:
public static int anyInt()
:返回任意int类型的数据 - argThat:
public static <T> T argThat(ArgumentMatcher<T> matcher)
:参数匹配器
OngoingStubbing:public interface OngoingStubbing<T>
:方法打桩时返回的接口
- thenReturn:
OngoingStubbing<T> thenReturn(T value)
:设置返回值,用作实参的方法调用必须有一个返回值 - thenThrow:
OngoingStubbing<T> thenThrow(Throwable... throwables)
:设置抛出的异常 - thenAnswer:
OngoingStubbing<T> thenAnswer(Answer<?> answer)
:设置返回值,可以根据参数进行计算 - thenCallRealMethod:
OngoingStubbing<T> thenCallRealMethod()
:设置,当被模拟出的对象上的方法被调用时,调用真实的方法
源码分析
mockito中的几个基本功能:
- 通过mock方法创建一个类的模拟实例
- 通过spy方法监视一个真实的对象
- when和thenReturn方法配合实现方法打桩。
- verify方法验证模拟对象的行为
mockito的基本原理,是生成被mock类的子类,用户持有这个子类的实例,通过这个子类,实现方法打桩的功能,所以mockito不支持模拟静态方法、私有方法、被final修饰的方法,因为它们无法被继承。接下来研究mockito究竟是怎么做到的。
mock方法
案例:
@Test
public void test1() throws InterruptedException {// 创建一个mock对象List mockList = Mockito.mock(List.class);// 打印mock对象的类名:org.mockito.codegen.List$MockitoMock$960824855System.out.println("mockList.getClass().getName() = " + mockList.getClass().getName());// 判断mock对象的类型assert mockList instanceof List;
}
整体流程:进入mock方法,经过一系列调用,进入MockitoCore的mock方法,这个方法中定义了创建mock实例的整体流程
// 参数1是要mock的类的类对象,在这里是List.class,参数2是默认配置
public <T> T mock(Class<T> typeToMock, MockSettings settings) {// 配置类实例是默认创建的,在这里判断如果它不是MockSettingsImpl类型,抛异常if (!MockSettingsImpl.class.isInstance(settings)) {throw new IllegalArgumentException("Unexpected implementation of '" +settings.getClass().getCanonicalName() + "'\nAt the moment, you cannot provide your own implementations of that class.");} else {// 获取配置类实例MockSettingsImpl impl = (MockSettingsImpl)MockSettingsImpl.class.cast(settings);// 构造创建mock实例时的配置信息MockCreationSettings<T> creationSettings = impl.build(typeToMock);// 创建mock实例T mock = MockUtil.createMock(creationSettings);// 将mock实例存放到ThreadLocal中ThreadSafeMockingProgress.mockingProgress().mockingStarted(mock, creationSettings);return mock;}
}
第一步:构造创建mock实例时的配置信息
// build方法最终调用validateSettings方法,根据类对象判断该类是否可以被mock
private static <T> CreationSettings<T> validatedSettings(Class<T> typeToMock, CreationSettings<T> source) {// 创建校验器MockCreationValidator validator = new MockCreationValidator();// 校验类对象的类型,底层是一个native方法,校验类对象是否可变,同时类对象不是String.class或包装类的类对象validator.validateType(typeToMock);validator.validateExtraInterfaces(typeToMock, source.getExtraInterfaces());validator.validateMockedType(typeToMock, source.getSpiedInstance());validator.validateConstructorUse(source.isUsingConstructor(), source.getSerializableMode());// 构造存储配置信息的实例CreationSettings<T> settings = new CreationSettings(source);settings.setMockName(new MockNameImpl(source.getName(), typeToMock, false));settings.setTypeToMock(typeToMock);settings.setExtraInterfaces(prepareExtraInterfaces(source));return settings;
}
第三步:探究第一步中 “创建mock实例” 时做了什么,T mock = MockUtil.createMock(creationSettings);
public static <T> T createMock(MockCreationSettings<T> settings) {// 创建mockHandlerMockHandler mockHandler = MockHandlerFactory.createMockHandler(settings);// 创建mock实例T mock = mockMaker.createMock(settings, mockHandler);Object spiedInstance = settings.getSpiedInstance();if (spiedInstance != null) {(new LenientCopyTool()).copyToMock(spiedInstance, mock);}return mock;
}
// 上一步中的createMock方法,经过一系列的调用,最终调用MockMaker中的crateMock方法
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {// 这里是使用字节码生成技术,创建一个类对象Class<? extends T> type = this.createMockType(settings);Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings);try {// 创建mock类的实例T instance = instantiator.newInstance(type);// 创建方法拦截器,用户通过mock实例调用方法时,在内部会先调用拦截器 MockMethodInterceptorMockMethodInterceptor mockMethodInterceptor = new MockMethodInterceptor(handler, settings);this.mocks.put(instance, mockMethodInterceptor);if (instance instanceof MockAccess) {((MockAccess)instance).setMockitoInterceptor(mockMethodInterceptor);}return instance;} catch (InstantiationException var7) {InstantiationException e = var7;throw new MockitoException("Unable to create mock instance of type '" + type.getSimpleName() + "'", e);}
}
mock方法创建出的实例:使用arthas来查看mockito创建出的字节码,具体方法是先打印出类的全限定名,然后在arthas中直接查看这个类的字节码信息
package org.mockito.codegen;import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.mockito.codegen.List$MockitoMock$1223363968$auxiliary$CVDZlt15;
import org.mockito.codegen.List$MockitoMock$1223363968$auxiliary$H1mHXgMT;
import org.mockito.internal.creation.bytebuddy.MockAccess;
import org.mockito.internal.creation.bytebuddy.MockMethodAdvice;
import org.mockito.internal.creation.bytebuddy.MockMethodInterceptor;public class List$MockitoMock$1223363968
implements List,
MockAccess {private static final long serialVersionUID = 42L;private MockMethodInterceptor mockitoInterceptor;private static final /* synthetic */ Method cachedValue$l5T7Iaqy$sgg2351;static {cachedValue$l5T7Iaqy$479u1c1 = List.class.getMethod("size", new Class[0]);cachedValue$l5T7Iaqy$2ff4l01 = List.class.getMethod("get", Integer.TYPE);cachedValue$l5T7Iaqy$sgg2351 = List.class.getMethod("add", Object.class); }// 这里省略了一些方法// 从生成的实例中可以看到,用户调用mock实例的方法时,在内部实际上是调用MockMethodOInterceptor中的方法,// 这里具体是调用MockMethodInterceptor的内部类DispatcherDefaultingToRealMethod中的// interceptAbstract方法@Overridepublic boolean add(Object object) {return (Boolean)MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptAbstract(this, this.mockitoInterceptor, false, cachedValue$l5T7Iaqy$sgg2351, new Object[]{object});}@Overridepublic void setMockitoInterceptor(MockMethodInterceptor mockMethodInterceptor) {this.mockitoInterceptor = mockMethodInterceptor;}
}
spy方法
spy方法底层也是调用mock方法,只不过传入的配置信息不同,spy方法传入的配置信息表示要调用mock对象的真实方法
@CheckReturnValue
public static <T> T spy(T object) {return MOCKITO_CORE.mock(object.getClass(),withSettings().spiedInstance(object).defaultAnswer(CALLS_REAL_METHODS));
}
when方法和thenReturn方法
测试案例:
@Test
public void test2() {List mockList = Mockito.mock(List.class);// 方法打桩:配置模拟对象上某个方法的行为,这里配置add("one")时返回trueMockito.when(mockList.add("one")).thenReturn(true);assert mockList.add("one"); // trueassert !mockList.add("two"); // false
}
方法打桩的整体流程:方法打桩时,首先执行mock对象上的方法,然后执行when方法,然后执行thenReturn方法。
- mock对象上的方法:mock对象是mockito生成的,它的内部会调用拦截器,记录当前方法的参数信息,生成invocation实例,存储到ThreadLocal中,
- 执行when方法:取出invocation实例,生成打桩对象OnGoingStub
- 执行thenReturn方法:把参数添加到invocation实例中,从而完成方法打桩。
- 最终,用户通过mock对象调用指定方法时,mock对象会根据方法名和参数信息,查看ThreadLocal中有没有存储相应的打桩信息,如果有,返回打桩时设置的返回值。
总结:核心原理是拦截器加ThreadLocal,mock对象内部调用拦截器来生成调用信息,把它放在ThreadLocal中,后面都是通过ThreadLocal在线程内传递参数的。
实战案例
springboot整合mockito
通过一个实际场景,来学习springboot整合mockito的作用。
假设有如下场景,现在有一个UserController,UserController有两个依赖,UserService和LogService,UserService是一个rpc接口,LogService是一个日志记录接口,不依赖外部环境,UserController中的方法都有注解,这些注解会被切面类处理,在切面类中实现权限校验功能。
流程图:
代码:
UserController
@RestController
@RequestMapping("/api/v1/user")
public class UserController {@Autowiredprivate IUserService userService;@Autowiredprivate ILogService logService;@PostMapping("/create")@AuthValidate(permission = PermissionEnum.CREATE_UPDATE_USER)public String create(@RequestBody String requestBody) {logService.log("接受到请求:" + requestBody);UserCreateVO userCreateVO = JsonUtil.fromJson(requestBody, UserCreateVO.class);UserDO userDO = convertUserCreateVO2DO(userCreateVO);int id;try {id = userService.create(userDO);} catch (Exception e) {logService.log("创建失败:" + e.getMessage());return ResponseBody.fail("创建失败:" + e.getMessage()).toJson();}return ResponseBody.success(id).toJson();}private UserDO convertUserCreateVO2DO(UserCreateVO userCreateVO) {UserDO userDO = new UserDO();BeanUtils.copyProperties(userCreateVO, userDO);Date date = new Date();userDO.setCreateUser("unknown");userDO.setCreateTime(date);userDO.setModifyUser("unknown");userDO.setModifyTime(date);return userDO;}
}
注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AuthValidate {PermissionEnum permission();
}
处理注解的切面类:
@Component
@Aspect
public class AuthValidateAspect {private static final Logger LOG = LoggerFactory.getLogger(AuthValidateAspect.class);@Pointcut("@annotation(org.wyj.beans.annotations.AuthValidate)")public void pointcut() { }@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return ResponseBody.fail("用户 " + "unknown" + " 没有权限").toJson();}HttpServletRequest request = attributes.getRequest();String userName = request.getHeader("userName");if ("zs".equals(userName)) {return joinPoint.proceed();} else {return ResponseBody.fail("用户 " + userName + " 没有权限").toJson();}}
}
现在的需求是,要为UserController编写单元测试。
使用mockito,可以很轻松的为UserController编写单元测试。代码如下:
public class UserControllerTest {@InjectMocks@Spyprivate UserController userController;@Mockprivate ILogService logService;@Mockprivate IUserService userService;@BeforeEachpublic void beforeEach() {MockitoAnnotations.openMocks(this);}// 正例:创建用户成功@Testpublic void test1() {// 模拟外部依赖UserDO userDO = new UserDO();userDO.setName("张三");userDO.setAge(18);Mockito.when(userService.create(Mockito.any())).thenReturn(1);Mockito.doNothing().when(logService).log("{log}");// 测试String s = userController.create(JsonUtil.toJson(userDO));// 断言assert s != null;ResponseBody responseBody = JsonUtil.fromJson(s, ResponseBody.class);Integer data = ((Double) responseBody.getData()).intValue();assert data.equals(1);}
}
在上面的单测中,用户可以直接运行单测,它是独立的,不依赖外部环境,包括spring容器,但是它有一个不足,无法验证注解是否生效,因为单测不是在spring容器中运行的。这就需要用到springboot整合mockito,单测在spring容器中运行,同时,使用mockito模拟外部依赖。要注意,其实这种方式在理论上已经脱离了单元测试的范畴,更加像是多个模块之间的集成测试,但是把这种测试提前放到单元测试中完成,是比较推荐的,避免把问题遗留到联调时。
添加依赖:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version><relativePath/>
</parent><artifactId>demo2</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring-boot提供的单测框架,框架中完成了对于mockito的整合--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.6</version></dependency><!--添加对于mock静态方法的支持--><dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>3.4.0</version><exclusions><!--前面spring-boot-starter-test中已经有关于mockito-core的依赖了--><exclusion><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId></exclusion></exclusions><scope>test</scope></dependency>
</dependencies>
编写单测:
@SpringBootTest(classes = App.class)
public class UserController2Test {@Autowiredprivate UserController userController;@MockBeanprivate IUserService userService;// 正例:权限校验成功,用户创建成功@Testpublic void test1() {try (MockedStatic<RequestContextHolder> theMock = Mockito.mockStatic(RequestContextHolder.class)) {// 模拟请求体HttpServletRequest request = Mockito.mock(HttpServletRequest.class);ServletRequestAttributes servletRequestAttributes = new ServletRequestAttributes(request);Mockito.when((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).thenReturn(servletRequestAttributes);Mockito.when(request.getHeader("userName")).thenReturn("zs");String requestBody = "{\n" +" \"name\": \"张三\",\n" +" \"age\": 18\n" +"}";Mockito.when(userService.create(Mockito.any())).thenReturn(1);// 调用目标方法String s = userController.create(requestBody);// 断言ResponseBody responseBody = JsonUtil.fromJson(s, ResponseBody.class);int id = ((Double) responseBody.getData()).intValue();assert id == 1;}}
}
在上面的单测中,使用MockBean声明要被mock并注入到UserController中的外部依赖,同时使用@SpringBootTest注解,指定单测运行在spring容器中,这样就可以测试注解是否可以正确地应用到UserController上。
这就是springboot整合mockito的作用,它可以在spring容器中,mock指定模块的外部依赖。
踩坑记录
匹配器不可以和常量混合使用
mockito报错 InvalidUseOfMatchersException,不正确地使用匹配器,例如any()、anyInt()等,匹配器不可以和常量混合使用
equestAttributes);
Mockito.when(request.getHeader(“userName”)).thenReturn(“zs”);
String requestBody = "{\n" +" \"name\": \"张三\",\n" +" \"age\": 18\n" +"}";Mockito.when(userService.create(Mockito.any())).thenReturn(1);// 调用目标方法String s = userController.create(requestBody);// 断言ResponseBody responseBody = JsonUtil.fromJson(s, ResponseBody.class);int id = ((Double) responseBody.getData()).intValue();assert id == 1;}
}
}
在上面的单测中,使用MockBean声明要被mock并注入到UserController中的外部依赖,同时使用@SpringBootTest注解,指定单测运行在spring容器中,这样就可以测试注解是否可以正确地应用到UserController上。这就是springboot整合mockito的作用,它可以在spring容器中,mock指定模块的外部依赖。# 踩坑记录## 匹配器不可以和常量混合使用mockito报错 InvalidUseOfMatchersException,不正确地使用匹配器,例如any()、anyInt()等,匹配器不可以和常量混合使用
相关文章:
单元测试之mockito
简介 mockito是一款模拟测试框架,用于Java开发中的单元测试。通过mockito,可以创建和配置一个对象,通过它来替换对象的外部依赖。 作用:模拟一个类的外部依赖,保证单元测试的独立性。例如,在类A中会调用类…...
定长池的实现
目录 一、定长池的框架 二、如何脱离malloc的内存池,直接从堆拿空间? 三、如何设计内存块的指针? 四、代码框架及实现 五、性能测试 一、定长池的框架 在学习高并发内存池之前,我们先来实现一个定长池࿰…...
C++多线程函数介绍
1.C11前没有线程库问题 对于多线程操作,Linux选择使用POSIX标准,而windows没有选择POSIX标准,自己设计了一套API和系统调用,叫Win32 API,就跟Linux存在标准差异,在Linux的代码移植到Windows就可能运行不了…...
图解AUTOSAR_SWS_LINTransceiverDriver
AUTOSAR LIN收发器驱动(LinTransceiverDriver)详解 AUTOSAR通信栈物理层组件详细解析 目录 AUTOSAR LIN收发器驱动(LinTransceiverDriver)详解 目录1. 概述 1.1. LIN收发器驱动的作用1.2. 在AUTOSAR架构中的位置2. 架构设计 2.1. 模块结构2.2. 组件关系2.3. 接口定义3. 状态管理…...
vue-element-admin 组件没有展示在中间部分
解决办法: router index.ts 中新增 要展示的组件的 import type { App } from "vue"; import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";export const Layout () > import("/layout/index.…...
当机器学习遇见购物车分析:FP-Growth算法全解析
一、引言:购物篮里的秘密 想象一下,你是一家超市的数据分析师,看着每天成千上万的购物小票,你是否好奇:顾客们买面包的时候,是不是也经常顺手带上牛奶?买啤酒的人,会不会也喜欢买尿…...
OCR迁移
一、环境 操作系统:Centos57.6 数据库版本:12.2.0.1 场景:将OCR信息从DATA磁盘组迁移到OCR磁盘组 二、操作步骤 1.查看可用空盘 set lin 200 set pagesize 200 col DGNAME format a15 col DISKNAME format a15 col PATH format a20 col N…...
【架构艺术】Go大仓monorepo中使用wire做依赖注入的经验
在先前的文章当中,笔者分享了一套简洁的go微服务monorepo代码架构的实现,主要解决中小团队协同开发微服务集群的代码架构组织问题。但是在实际代码开发过程中,怎么组织不同的业务服务service实例,就成了比较棘手的问题。 为什么会…...
生活电子常识--删除谷歌浏览器搜索记录
前言 谷歌浏览器会记录浏览器历史搜索,如果不希望看到越来越多的搜索记录,可以如下设置 解决 设置-隐私-自动填充表单 这个和浏览器记录的密码没有关系,可以放心删除...
每日一题(小白)模拟娱乐篇13
今天题目比较简单,直接分析。小蓝想知道2024这个数字中有几个1,计算机组成学习好的同学肯定可以直接长除法或者瞪眼法得出答案: 202411111101000(B)也就是说2024中有一共有六个1 接下来用代码实现 ,我们也…...
码曰编程大模型-学编程的好工具
码曰(yue),一款编程垂直领域的AI大模型,是基于包括DeepSeek在内的多款国产大模型为底座,依托于Dotcpp系统大量的编程代码数据,且借助RAG数据检索增强等技术综合实现的出色、好用的编程垂直领域AI大模型&…...
Linux(CentOS 7) 部署 redis 集群
下载redis Downloads - Redis (官网页都是介绍的最新版,我观察目前出现了redis 和 redis Stack) 因我的旧环境是 CentOS 7,redis最新版已经不在支持,所以示例安装最常用的7.0.x 这里直接附上各个版本下载连接 小伙伴们就不需要在自己寻找下载…...
NVIDIA AgentIQ 详细介绍
NVIDIA AgentIQ 详细介绍 1. 引言 NVIDIA AgentIQ 是一个灵活的库,旨在将企业代理(无论使用何种框架)与各种数据源和工具无缝集成。通过将代理、工具和代理工作流视为简单的函数调用,AgentIQ 实现了真正的可组合性:一…...
在CPU服务器上部署Ollama和Dify的过程记录
在本指南中,我将详细介绍如何在CPU服务器上安装和配置Ollama模型服务和Dify平台,以及如何利用Docker实现这些服务的高效部署和迁移。本文分为三大部分:Ollama部署、Dify环境配置和Docker环境管理,适合需要在本地或私有环境中运行A…...
小程序API —— 57 拓展 - 增强 scroll-view
目录 1. 配置基本信息2. 实现上拉加载更多功能3. 实现快速回到顶部功能4. 实现下拉刷新功能 scroll-view 组件功能非常强大,这里使用 scroll-view 实现上拉加载和下拉刷新功能; 下面使用微信开发者工具来演示一下具体用法: 1. 配置基本信息 …...
P3613 【深基15.例2】寄包柜
#include<bits/stdc.h> using namespace std; int n,q; map<int, map<int, int>>a;//二维映射 int main(){cin>>n>>q;while(q--){int b,i,j,k;//i为第几个柜子,j为第几个柜包,k为要存入的物品cin>>b>>i>&…...
MIMO预编码与检测算法的对比
在MIMO系统中,预编码(发送端处理)和检测算法(接收端处理)的核心公式及其作用对比如下: 1. 预编码算法(发送端) 预编码的目标是通过对发送信号进行预处理,优化空间复用或…...
AI复活能成为持续的生意吗?
随着人工智能技术的飞速发展,AI复活——这一曾经只存在于科幻电影中的概念,如今已悄然走进现实。通过AI技术,人们可以模拟逝去亲人的声音、面容,甚至创造出与他们互动的虚拟形象,以寄托哀思、缓解痛苦。然而,当这种技术被商业化,成为一门生意时,我们不禁要问:AI复活真…...
Keil 5 找不到编译器 Missing:Compiler Version 5 的解决方法
用到自记: 下载地址: Keil5 MDK541.zip 编辑https://pan.baidu.com/s/1bOPsuVZhD_Wj4RJS90Mbtg?pwdMDK5 问题描述 没有找到 compiler version5 : 1. 下载 Arm Compiler 5 也可以直接点击下载文章开头的文件。 2. 安装 直接安装在KEI…...
Flutter 手搓日期选择
时间选择器: 时间段选择 在实际开发过程中还是挺常见的。Flutter 本身自带选 时间选择器器 CupertinoDatePicker,样式也是可以定义的,但是他 只提供三种时间的选择 自定义有局限性。后来开了一下 代码,实际上 内部使用的是 Cuper…...
《JVM考古现场(十六):太初奇点——从普朗克常量到宇宙弦的编译风暴》
开篇:量子泡沫编译器的创世大爆炸 "当Project Genesis的真空涨落算法撕裂量子泡沫,当意识编译器重写宇宙基本常数,我们将在奇点编译中见证:从JVM字节码到宇宙大爆炸的终极创世!诸君请备好量子护目镜,…...
MySQL学习笔记——MySQL下载安装配置(一)
目录 1. MySQL概述 1.1 数据库相关概念 1.2 MySQL数据库 1.2.1 版本 1.2.2 下载 2. 安装 3. 配置 4. 启动停止 5. 客户端连接 1. MySQL概述 1.1 数据库相关概念 在这一部分,我们先来讲解三个概念:数据库、数据库管理系统、 SQL 。 而目前主流…...
TortoiseGit多账号切换配置
前言 之前配置好的都是,TortoiseGit与Gitee之间的提交,突然有需求要在GitHub上提交,于是在参考网上方案和TortoiseGit的帮助手册后,便有了此文。由于GitHub已经配置完成,所以下述以配置Gitee为例。因为之前是单账号使用…...
数据一键导出为 Excel 文件
引言 在 Web 应用开发中,数据导出是一个常见且重要的功能。用户常常需要将网页上展示的数据以文件形式保存下来,以便后续分析、处理或分享。本文将详细介绍如何使用 HTML、CSS 和 JavaScript(结合 jQuery 库)实现一个简单的数据导…...
FPGA——状态机实现流水灯
文章目录 一、状态机1.1 分类1.2 写法 二、状态机思想编写LED流水灯三、运行结果总结参考资料 一、状态机 FPGA不同于CPU的一点特点就是CPU是顺序执行的,而FPGA是同步执行(并行)的。那么FPGA如何处理明显具有时间上先后顺序的事件呢…...
linux paste 命令
paste 是 Linux 中一个用于水平合并文件内容的命令行工具,它将多个文件的对应行以并行方式拼接,默认用制表符(Tab)分隔。 1. 基本语法 paste [选项] 文件1 文件2 ... 2. 常用选项 选项说明-d指定拼接后的分隔符(默…...
ffmpeg常见命令2
文章目录 1. **提取音视频数据(Extract Audio/Video Data)**提取音频:提取视频: 2. **提取像素数据(Extract Pixel Data)**3. **命令转封装(Container Format Conversion)**转换视频…...
FPGA——FPGA状态机实现流水灯
一、引言 在FPGA开发中,状态机是一种重要的设计工具,用于处理具有时间顺序的事件。本文将详细介绍如何使用状态机实现一个LED流水灯的效果。 二、状态机概述 状态机(FSM)是一种行为模型,用于表示系统在不同状态下的…...
鸿蒙 ——选择相册图片保存到应用
photoAccessHelper // entry/src/main/ets/utils/file.ets import { fileIo } from kit.CoreFileKit; import { photoAccessHelper } from kit.MediaLibraryKit; import { bundleManager } from kit.AbilityKit;// 应用在本设备内部存储上通用的存放默认长期保存的文件路径&am…...
消息队列之-Kafka
目录 消息队列消息队列的使用场景初识KafkaKafka设计思想Kafka消息结构消息发送消息消费 Kafka高可用消息备份机制1. 基本原理2. ISR(In-Sync Replicas)3. ACK(Acknowledgements)4. LEO(Log End Offset)5. …...
财务税务域——企业税务系统设计
摘要 本文主要探讨企业税务系统设计,涵盖企业税收管理背景、税收业务流程、系统设计架构与功能、外部系统对接以及相关问题。企业税务的背景包括税收制度的形成、企业税务的必然性、全球化影响,其核心目标是合规性、优化税负、风险管理与战略支持&#…...
状态机思想编程
文章目录 一、状态机思想重新写一个 LED流水灯的FPGA代码1.状态机的概念2.代码设计 二、CPLD和FPGA芯片的主要技术区别与适用场合三、hdlbitsFPGA教程网站上进行学习 一、状态机思想重新写一个 LED流水灯的FPGA代码 1.状态机的概念 状态机的基本要素有 3 个,其实我…...
TiDB 数据库8.1版本编译及部署
本文介绍 TiDB 数据库8.1版本的编译和部署。 背景 自前年(2023年)接触了TiDB后,做了简单的测试就直接使用了。因一些事务的不连续性,导致部分成果没有保存,去年年底又重新拾起,使用了新的LTS版本ÿ…...
基于 docker 的 Xinference 全流程部署指南
Xorbits Inference (Xinference) 是一个开源平台,用于简化各种 AI 模型的运行和集成。借助 Xinference,您可以使用任何开源 LLM、嵌入模型和多模态模型在云端或本地环境中运行推理,并创建强大的 AI 应用。 一、下载代码 请在控制台下面执行…...
【2022】【论文笔记】基于相变材料的光学激活的、用于THz光束操作的编码超表面——
前言 类型 太赫兹 + 超表面 太赫兹 + 超表面 太赫兹+超表面 期刊 A D V A N C E D O P T I C A L M A T E R I A L S ADVANCED \; OPTICAL \; MATERIALS...
MySQL系统库汇总
目录 简介 performance_schema 作用 分类 简单配置与使用 查看最近执行失败的SQL语句 查看最近的事务执行信息 sys系统库 作用 使用 查看慢SQL语句慢在哪 information_schema 作用 分类 应用 查看索引列的信息 mysql系统库 权限系统表 统计信息表 日志记录…...
【Kafka基础】Docker Compose快速部署Kafka单机环境
1 准备工作 1.1 安装Docker和Docker Compose Docker安装请参考: Docker入门指南:1分钟搞定安装 常用命令,轻松入门容器化!-CSDN博客 Docker Compose安装请参考: 【docker compose入门指南】安装与常用命令参数全解析…...
【51单片机】2-5【I/O口】433无线收发模块控制继电器
1.硬件 51最小系统继电器模块433无线收发模块 2.软件 #include "reg52.h"sbit D0_ON P1^2;//433无线收发模块的按键A sbit D1_OFF P1^3;//433无线收发模块的按键Bsbit switcher P1^1;//继电器void main() {//查询方式哪个按键被按下while(1){if(D0_ON 1)//收到…...
平台总线---深入分析
阅读引言:本文会从平台总线的介绍,注册平台设备和驱动, 源码分析, 总结五个部分展开, 源码分析platform放在了最后面。 目录 一、平台总线介绍 二、平台总线如何使用 三、平台总线是如何工作的 四、注册platform设…...
pyTorch框架:模型的子类写法--改进版二分类问题
目录 1.导包 2.加载数据 3.数据的特征工程 4.pytorch中最常用的一种创建模型的方式--子类写法 1.导包 import torch import pandas as pd import numpy as np import matplotlib.pyplot as plt2.加载数据 data pd.read_csv(./dataset/HR.csv)data.head() #查看数据的前…...
【python中级】解压whl文件内容
【python中级】解压whl文件内容 1.背景2.解压1.背景 【python中级】关于whl文件的说明 https://blog.csdn.net/jn10010537/article/details/146979236 补充以上博客: 在 旧版 setuptools 中(< v58),如果想生成 .whl,必须先pip install 安装 wheel 三方包! pip inst…...
【USRP】srsRAN 开源 4G 软件无线电套件
srsRAN 是SRS开发的开源 4G 软件无线电套件。 srsRAN套件包括: srsUE - 具有原型 5G 功能的全栈 SDR 4G UE 应用程序srsENB - 全栈 SDR 4G eNodeB 应用程序srsEPC——具有 MME、HSS 和 S/P-GW 的轻量级 4G 核心网络实现 安装系统 Ubuntu 20.04 USRP B210 sudo …...
LeetCode算法题(Go语言实现)_30
题目 给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。 第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。 请注意,偶数组和奇数组内…...
生信入门:专栏概要与内容目录
文章目录 生信入门📚 核心内容模块基础概念入门序列联配算法高级算法与应用理论基础与数学方法基因组分析 生信入门 🔥 专栏简介 | 生信算法与实践指南 开启生物信息学的学习之旅 🌟 为什么订阅本专栏? 循序渐进:从生…...
Matplotlib:数据可视化的艺术与科学
引言:让数据开口说话 在数据分析与机器学习领域,可视化是理解数据的重要桥梁。Matplotlib 作为 Python 最流行的绘图库,提供了从简单折线图到复杂 3D 图表的完整解决方案。本文将通过实际案例,带您从基础绘图到高级定制全面掌握 …...
线程共享数据所带来的安全性问题
笔记 import threading from threading import Thread import time tickte50 # 代表的是50张票def sale_ticket():global tickte# 每个排队窗口假设有100人for i in range(100): # 每个线程要执行100次循环if tickte>0:print(f{threading.current_thread().name}正在出售第…...
Redis核心机制-缓存、分布式锁
目录 缓存 缓存更新策略 定期生成 实时生成 缓存问题 缓存预热(Cache preheating) 缓存穿透(Cache penetration) 缓存雪崩(Cache avalanche) 缓存击穿(Cache breakdown) 分…...
Node.js中间件的5个注意事项
目录 1. 目录结构 2. 代码实现 注意事项 1:必须调用 next() 注意事项 2:中间件的执行顺序很重要 注意事项 3:局部中间件的使用 注意事项 4:统一处理 404 注意事项 5:使用错误处理中间件 3. 总结 在Node.js的Ex…...
软件学报 2024年 区块链论文 录用汇总 附pdf下载
Year:2024 1 Title: 带有预验证机制的区块链动态共识算法 Authors: Key words: 区块链;混合共识;预验证机制;动态共识;委员会腐败 Abstract: 委员会共识和混合共识通过选举委员会来代替全网节点完成区块验证, 可有效加快共识速度, 提高吞吐量, 但恶意攻击和收…...
从开发到上线:基于 Linux 云服务器的前后端分离项目部署实践(Vue + Node.js)
明白了,这次我们完全聚焦技术内容本身,不带明显广告语言,不插入链接,只在文末一个不显眼的地方轻描淡写提到“服务器用的是 zovps.com 的一台基础云主机”,整体文章保证原创、高质量、易审核、易分发,长度控…...