单元测试之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()等,匹配器不可以和常量混合使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/75857.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Oracle数据库数据编程SQL<5 正则表达式函数*****>

Oracle 提供了一组强大的正则表达式函数,用于在 SQL 和 PL/SQL 中进行复杂的模式匹配和文本处理。这些函数基于 POSIX 标准正则表达式,功能强大且灵活。 目录 一、Oracle 正则表达式函数概览 二、函数详解及示例 1. REGEXP_LIKE 2. REGEXP_INSTR 3. REGEXP_SUBSTR 4. …

el-tabs添加按钮增加点击禁止样式

前置文章 一、vue使用element-ui自定义样式思路分享【实操】 二、vue3&ts&el-tabs多个tab表单校验 现状确认 点击添加按钮&#xff0c;没有点击样式&#xff0c;用户感知不明显没有限制最大的tab添加数量&#xff0c;可以无限添加 调整目标&代码编写 调整目标…

DB-Mysql中TIMESTAMP与DATETIME的区别

文章目录 ‌存储范围‌‌时区处理‌存储空间‌默认值和自动更新‌‌零值处理‌适用场景‌总结 在MySQL中&#xff0c;TIMESTAMP和DATETIME是两种常用的日期时间数据类型&#xff0c;它们虽然都用于存储日期和时间&#xff0c;但在多个方面存在显著差异。以下是它们的主要区别&a…

Spring 中有哪些设计模式?

&#x1f9e0; 一、Spring 中常见的设计模式 设计模式类型Spring 中的应用场景单例模式创建型默认 Bean 是单例的工厂模式创建型BeanFactory、FactoryBean抽象工厂模式创建型ApplicationContext 提供多个工厂接口代理模式结构型AOP 动态代理&#xff08;JDK/CGLIB&#xff09;…

C# Winform 入门(3)之尺寸同比例缩放

放大前 放大后 1.定义当前窗体的宽度和高度 private float x;//定义当前窗体的宽度private float y;//定义当前窗台的高度 2.接收当前窗体的尺寸大小 x this.Width;//存储原始宽度ythis.Height;//存储原始高度setTag(this);//为控件设置 Tag 属性 3.声明方法&#xff0c;获…

从零开始的编程-java篇1.6.3

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

【Redis】数据的淘汰策略

目录 淘汰策略方案&#xff08;8种&#xff09; LRU和LFU策略的区别 使用建议 手搓LRU算法 方式一 方式二 大家好&#xff0c;我是jstart千语。今天和大家回来聊一下redis&#xff0c;这次要讲的是它的淘汰策略。为什么需要淘汰策略呢&#xff0c;就是当redis里面的内存占…

【前端】Node.js一本通

近两天更新完毕&#xff0c;建议关注收藏点赞。 目录 复习Node.js概述使用fs文件系统模块path路径模块 http模块 复习 为什么JS可以在浏览器中执行 原理&#xff1a;待执行的JS代码->JS解析引擎 不同的浏览器使用不同的 JavaScript 解析引擎&#xff1a;其中&#xff0c;C…

【AI论文】JavisDiT: 具备层次化时空先验同步机制的联合音视频扩散Transformer

摘要&#xff1a;本文介绍了一种新型的联合音频-视频扩散变换器JavisDiT&#xff0c;该变换器专为同步音频-视频生成&#xff08;JAVG&#xff09;而设计。 基于强大的扩散变换器&#xff08;DiT&#xff09;架构&#xff0c;JavisDiT能够根据开放式用户提示同时生成高质量的音…

Java-实现公有字段自动注入(创建人、创建时间、修改人、修改时间)

文章目录 Mybatis-plus实现自动注入定义 MetaObjectHandler配置 MyBatis-Plus 使用 MetaObjectHandler实体类字段注解使用服务类进行操作测试 Jpa启用审计功能实现自动注入添加依赖启动类启用审计功能实现AuditorAware接口实体类中使用审计注解 总结 自动注入创建人、创建时间、…

金融机构开源软件风险管理体系建设

开源软件为金融行业带来了创新活力的同时&#xff0c;也引入了一系列独特的风险。金融机构需要构建系统化的风险管理体系&#xff0c;以识别和应对开源软件在全生命周期中的各种风险点。下面我们将解析开源软件在金融场景下的主要风险类别&#xff0c;并探讨如何建立健全的风险…

图形渲染中的定点数和浮点数

三种API的NDC区别 NDC全称&#xff0c;Normalized Device Coordinates Metal、Vulkan、OpenGL的区别如下&#xff1a; featureOpenGL NDCMetal NDCVulkan NDC坐标系右手左手右手z值范围[-1,1][0,1][0,1]xy视口范围[-1,1][-1,1][-1,1] GPU渲染的定点数和浮点数 定点数类型&a…

同花顺客户端公司财报抓取分析

目标客户端下载地址:https://ft.51ifind.com/index.php?c=index&a=download PC版本 主要难点在登陆,获取token中的 jgbsessid (每次重新登录这个字段都会立即失效,且有效期应该是15天的) 抓取jgbsessid 主要通过安装mitmproxy 使用 mitmdump + 下边的脚本实现监听接口…

QT工程建立

打开软件新建一个工程 选择chose 工程命名&#xff0c;选择保存路径&#xff0c;可以自己选择&#xff0c;但是不要有中文路径 默认的直接下一步 任意选一个下一步 点击完成 之后是这个界面&#xff0c;点击右下角的绿色三角形编译一下 实验内容 添加类 第一个是建立cpp和.h文件…

【NLP 53、投机采样加速推理】

目录 一、投机采样 二、投机采样改进&#xff1a;美杜莎模型 流程 改进 三、Deepseek的投机采样 流程 Ⅰ、输入文本预处理 Ⅱ、引导模型预测 Ⅲ、候选集筛选&#xff08;可选&#xff09; Ⅳ、主模型验证 Ⅴ、生成输出与循环 骗你的&#xff0c;其实我在意透了 —— 25.4.4 一、…

ffmpeg时间基与时间戳

时间基、时间戳 时间基&#xff1a;表示时间单位的分数&#xff0c;用来定义视频或音频流中时间的精度。其形式是一个分数&#xff0c;分子通常为 1&#xff0c;而分母则表示每秒的单位数。 时间戳&#xff1a;代表在时间轴里占了多少个格子&#xff0c;是特定的时间点。 时间…

激光加工中平面倾斜度的矫正

在激光加工中&#xff0c;加工平面的倾斜度矫正至关重要&#xff0c;直接影响加工精度和材料处理效果。以下是系统的矫正方法和步骤&#xff1a; 5. 验证与迭代 二次测量&#xff1a;加工后重新检测平面度&#xff0c;确认残余误差。 反馈优化&#xff1a;根据误差分布修正补偿…

算法刷题记录——LeetCode篇(2.2) [第111~120题](持续更新)

更新时间&#xff1a;2025-04-04 算法题解目录汇总&#xff1a;算法刷题记录——题解目录汇总技术博客总目录&#xff1a;计算机技术系列博客——目录页 优先整理热门100及面试150&#xff0c;不定期持续更新&#xff0c;欢迎关注&#xff01; 114. 二叉树展开为链表 给你二…

C语言学习笔记-9

九、结构体 构造类型&#xff1a; 不是基本类型的数据结构也不是指针类型&#xff0c; 它是若干个相同或不同类型的数据构成的集合 结构体类型&#xff1a; 结构体是一种构造类型的数据结构&#xff0c;是一种或多种基本类型或构造类型的数据的集合。 1.结构体类型定义 定…

Test——BUG篇

目录 一软件测试的生命周期 二BUG 1概念 2描述Bug 3Bug级别 4Bug的生命周期 三与开发人员发生争执怎么办 ​编辑1先自省&#xff1a;是否Bug描述不清晰 2站在用户角度考虑并抛出问题 3Bug定级有理有据 4不仅要提出问题&#xff0c;还要给出解决方案 5Bug评审 5.1…