单元测试之mockito

发布于:2025-04-06 ⋅ 阅读:(21) ⋅ 点赞:(0)

简介

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")时返回true
    Mockito.when(mockList.add("one")).thenReturn(true);

    assert mockList.add("one");  // true
    assert !mockList.add("two"); // false

    // 方法打桩,配置模拟对象调用size()方法时返回1
    Mockito.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>() {
                @Override
                public 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,让其返回99
        Mockito.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;

    @Mock
    private AccountMapper mapper;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);  // 使用注解前,创建环境,使注释生效
    }

    @Test
    public 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$960824855
    System.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) {
    // 创建mockHandler
    MockHandler 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实例调用方法时,在内部会先调用拦截器 MockMethodInterceptor
        MockMethodInterceptor 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方法
    @Override
    public boolean add(Object object) {
        return (Boolean)MockMethodInterceptor.DispatcherDefaultingToRealMethod.interceptAbstract(this, this.mockitoInterceptor, false, cachedValue$l5T7Iaqy$sgg2351, new Object[]{object});
    }

    @Override
    public 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")时返回true
    Mockito.when(mockList.add("one")).thenReturn(true);

    assert mockList.add("one");  // true
    assert !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 {

    @Autowired
    private IUserService userService;

    @Autowired
    private 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
    @Spy
    private UserController userController;

    @Mock
    private ILogService logService;

    @Mock
    private IUserService userService;

    @BeforeEach
    public void beforeEach() {
        MockitoAnnotations.openMocks(this);
    }

    // 正例:创建用户成功
    @Test
    public 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 {
    @Autowired
    private UserController userController;

    @MockBean
    private IUserService userService;

    // 正例:权限校验成功,用户创建成功
    @Test
    public 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()等,匹配器不可以和常量混合使用