Mockito的使用及原理浅析

Mockito的使用及原理浅析

在微服务横行的年代,一个服务可能依赖了若干个其它服务,而这些被依赖的服务,又极可能依赖了别的服务,于是构成了一个复杂的依赖链。而日常开发中,经常会苦恼于某某上游接口没数据而所使得UT达不到预期的目标,所兴现在已经有了多种Mock Toolkit,很好的解决了这一痛点。比较常用的有EasyMock,JMockit,Mockito,PowerMock等,而本文主要介绍Mockito的简单使用,并对其实现原理进行简单分析

Mockito的使用

通常情况下,我们会在UT里使用@Mock、@Spy以及@InjectMocks等注解来Mock掉一些不可控的上下游接口,为了简化说明,我们将例子简单化。
假设有这样的一个服务

public class ServiceA {

    @Autowired
    private ServiceB serviceB;
    
    public String hello(String name) {
        return "ServiceA.hello:" + name;
    }
    
    public String helloB(String name) {
        return serviceB.hello(name);
    }
}

@Mock

现在我们来将它mock掉

public class MockTest {
    
    @Mock
    private ServiceA seriveA;

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void testA() {
        Mockito.when(seriveA.hello("aaa")).thenReturn("111").thenReturn("222");  //打桩
        System.out.println(seriveA.hello("aaa"));   //111                        //真实调用
        System.out.println(seriveA.hello("aaa"));   //222
        System.out.println(seriveA.hello("bbb"));   //null
    }
}

为了便于描述,先简单说明一下:上面serviceA.hello(“aaa”)调用结果做为入参传递给when()方法,我们称为打桩,而后面的调用是我们称为真实调用。
Mockito提供了通俗易用的API,@Mock注释用于对目标对象进行mock,生成立mock对象,when…thenReturn…用于模拟返回结果,当调用某某方法时返回我们想要的预定结果, thenThrow则用于模拟目标方法抛出异常,而我们也可以使用方法链模拟返回多个mock结果,在多次调用目标方法时,Mockito会依次为我们返回预先设定的mock结果。

@InjectMocks

ServiceA依赖了ServiceB,如果我们要mock的不是ServiceA,而是ServiceB,那我们可以在UT里,给ServiceB加注释@Mock,给ServiceA加上注释@InjectMocks,这时Mockito会自动为ServiceA注入mock对象ServiceB

public class MockTest2 {
    
    @InjectMocks
    private ServiceA serviceA;
    @Mock
    private ServiceB serviceB;
    
    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void testB() {
        Mockito.when(serviceB.hello("aaa")).thenReturn("111").thenReturn("222");
        System.out.println(serviceA.helloB("aaa"));   //111
        System.out.println(serviceA.helloB("aaa"));   //222
        System.out.println(serviceA.helloB("bbb"));   //null
    }
}

从上面的用法中,整个mock的过程大概可以分为三步:
1)@Mock注解声明了要将对象进行mock
2)使用MockitoAnnotations.initMocks告诉Mockito要对当前对象进行mock处理。当然你用可以使用@RunWith(MockitoJUnitRunner.class),殊途同归。
3)使用when…thenReturn来模拟返回
接下来我们来分析一下其实现原理。

Mockito的实现原理

mock对象的创建

首先我们看看MockitoAnnotations.initMocks到底做了什么?一起进入源码。

public static void initMocks(Object testClass) {
        if (testClass == null) {
            throw new MockitoException("testClass cannot be null. For info how to use @Mock annotations see examples in javadoc for MockitoAnnotations class");
        }

        AnnotationEngine annotationEngine = new GlobalConfiguration().getAnnotationEngine();
        Class<?> clazz = testClass.getClass();

        //below can be removed later, when we get read rid of deprecated stuff
        if (annotationEngine.getClass() != new DefaultMockitoConfiguration().getAnnotationEngine().getClass()) {
            //this means user has his own annotation engine and we have to respect that.
            //we will do annotation processing the old way so that we are backwards compatible
            while (clazz != Object.class) {
                scanDeprecatedWay(annotationEngine, testClass, clazz);
                clazz = clazz.getSuperclass();
            }
        }

        //anyway act 'the new' way
        annotationEngine.process(testClass.getClass(), testClass);
    }

这里有个AnnotationEngine,从名称中我们大概可以猜到Mockito是要对Annotation比如:@Mock,@Spy,@InjectMocks等进行处理,以此创建出Mock对象。
AnnotationEngine有三个实现,DefaultAnnotationEngine,SpyAnnotationEngine,InjectingAnnotationEngine,这三个AnnotationEngine合分别处理@Mock、@Spy和InjectMocks注解。这里选择DefaultAnnotationEngine进行分析。

public void process(Class<?> clazz, Object testInstance) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            boolean alreadyAssigned = false;
            for(Annotation annotation : field.getAnnotations()) {           
                Object mock = createMockFor(annotation, field);
                if (mock != null) {
                    throwIfAlreadyAssigned(field, alreadyAssigned);                    
                    alreadyAssigned = true;                    
                    try {
                        new FieldSetter(testInstance, field).set(mock);
                    } catch (Exception e) {
                        throw new MockitoException("Problems setting field " + field.getName() + " annotated with "
                                + annotation, e);
                    }
                }        
            }
        }
    }

在这里看到createMockFor,由于代码还比较长,略去代码,直接看下面的流程图
initMocks流程
从流程是我们大概知道,MockitoAnnotations.initMocks(this)的过程就是创建mock对象的过程,Mockito在1.x版本中是采用cglib动态创建被mock的目标类的代理类(2.x版本改用了bytebuddy),代理类是被mock目标类的子类,因此Mocktio无法mock被定义为final的类和方法。而在创建目标mock对象时,Mocktio利用cglib给mock对象加上了方法拦截器(MethodInterceptorFilter),这样当mock对象的方法被调用时,会被拦截器进行拦截处理。其中关键的代理类的生成和对象实例是创建是在ClassImposterizer中完成的,代码如下:

public Class<Factory> createProxyClass(Class<?> mockedType, Class<?>... interfaces) {
        if (mockedType == Object.class) {
            mockedType = ClassWithSuperclassToWorkAroundCglibBug.class;
        }
        
        Enhancer enhancer = new Enhancer() {
            @Override
            @SuppressWarnings("unchecked")
            protected void filterConstructors(Class sc, List constructors) {
                // Don't filter
            }
        };
        Class<?>[] allMockedTypes = prepend(mockedType, interfaces);
		enhancer.setClassLoader(SearchingClassLoader.combineLoadersOf(allMockedTypes));
        enhancer.setUseFactory(true);
        if (mockedType.isInterface()) {
            enhancer.setSuperclass(Object.class);
            enhancer.setInterfaces(allMockedTypes);
        } else {
            enhancer.setSuperclass(mockedType);
            enhancer.setInterfaces(interfaces);
        }
        enhancer.setCallbackTypes(new Class[]{MethodInterceptor.class, NoOp.class});
        enhancer.setCallbackFilter(IGNORE_BRIDGE_METHODS);
        if (mockedType.getSigners() != null) {
            enhancer.setNamingPolicy(NAMING_POLICY_THAT_ALLOWS_IMPOSTERISATION_OF_CLASSES_IN_SIGNED_PACKAGES);
        } else {
            enhancer.setNamingPolicy(MockitoNamingPolicy.INSTANCE);
        }

        enhancer.setSerialVersionUID(42L);
        
        try {
            return enhancer.createClass(); 
        } catch (CodeGenerationException e) {
            if (Modifier.isPrivate(mockedType.getModifiers())) {
                throw new MockitoException("\n"
                        + "Mockito cannot mock this class: " + mockedType 
                        + ".\n"
                        + "Most likely it is a private class that is not visible by Mockito");
            }
            throw new MockitoException("\n"
                    + "Mockito cannot mock this class: " + mockedType 
                    + "\n" 
                    + "Mockito can only mock visible & non-final classes."
                    + "\n" 
                    + "If you're not sure why you're getting this error, please report to the mailing list.", e);
        }
    }
    
    private Object createProxy(Class<Factory> proxyClass, final MethodInterceptor interceptor) {
        Factory proxy;
        try {
            proxy = instantiator.newInstance(proxyClass);
        } catch (InstantationException e) {
            throw new MockitoException("Unable to create mock instance of type '" + proxyClass.getSuperclass().getSimpleName() + "'", e);
        }
        proxy.setCallbacks(new Callback[] {interceptor, SerializableNoOp.SERIALIZABLE_INSTANCE });
        return proxy;
    }

when().thenReturn()

分析完mock对象的构建,接下来说when()…thenReturn()…,其中when()方法入参是Object对象,我们一般是先调用目标方法然后将其返回值传给when()(该步骤也被称为打桩),而对目标方法的调用会先被拦截器(MethodInterceptorFilter)拦截,最后由MockHandlerImpl进行处理,代码如下:

public Object handle(Invocation invocation) throws Throwable {
		if (invocationContainerImpl.hasAnswersForStubbing()) {
            // stubbing voids with stubVoid() or doAnswer() style
            InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(
                    mockingProgress.getArgumentMatcherStorage(),
                    invocation
            );
            invocationContainerImpl.setMethodForStubbing(invocationMatcher);
            return null;
        }
        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) {
            // We need to check if verification was started on the correct mock
            // - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
            if (((MockAwareVerificationMode) verificationMode).getMock() == invocation.getMock()) {
                VerificationDataImpl data = createVerificationData(invocationContainerImpl, invocationMatcher);
                verificationMode.verify(data);
                return null;
            } else {
                // this means there is an invocation on a different mock. Re-adding verification mode
                // - see VerifyingWithAnExtraCallToADifferentMockTest (bug 138)
                mockingProgress.verificationStarted(verificationMode);
            }
        }

        // prepare invocation for stubbing
        invocationContainerImpl.setInvocationForPotentialStubbing(invocationMatcher);
        OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl<T>(invocationContainerImpl);
        mockingProgress.reportOngoingStubbing(ongoingStubbing);

        // look for existing answer for this invocation
        StubbedInvocationMatcher stubbedInvocation = invocationContainerImpl.findAnswerFor(invocation);

        if (stubbedInvocation != null) {
            stubbedInvocation.captureArgumentsFrom(invocation);
            return stubbedInvocation.answer(invocation);
        } else {
             Object ret = mockSettings.getDefaultAnswer().answer(invocation);

            // redo setting invocation for potential stubbing in case of partial
            // mocks / spies.
            // Without it, the real method inside 'when' might have delegated
            // to other self method and overwrite the intended stubbed method
            // with a different one. The reset is required to avoid runtime exception that validates return type with stubbed method signature.
            invocationContainerImpl.resetInvocationForPotentialStubbing(invocationMatcher);
            return ret;
        }
	}

主要逻辑是判断当前的调用是打桩还是真实调用,如果是打桩,则将当前调用信息(Invocation)交给InvocationContainerImpl进行记录,如果不是,则从InvocationContainerImpl中查找StubbedInvocationMatcher,而StubbedInvocationMatcher维护着一个由mock方法的mock结果组成的队列,可以根据客户端的调用依次返回mock结果。
thenReturn()是OngoingStubbing接口定义的方法,由BaseStubbing抽象类实现,并最终会调用OngoingStubbingImpl的thenAnswer(),最后将mock交给InvocationContainerImpl进行记录。OngoingStubbingImpl部分源码如下:

public OngoingStubbing<T> thenAnswer(Answer<?> answer) {
        if(!invocationContainerImpl.hasInvocationForPotentialStubbing()) {
            new Reporter().incorrectUseOfApi();
        }

        invocationContainerImpl.addAnswer(answer);
        return new ConsecutiveStubbing<T>(invocationContainerImpl);
    }

整个when().thenReturn()的过程如下:
在这里插入图片描述
执行完when()…thenReturn…,则完成了打桩,接下来调用目标方法,MockHandlerImpl从InvoncationContainerImpl查询到mock结果,直接返回给客户端(TestCase)。
了解完整个mock的实现过程,可以自己实现一个简单的mock工具。

mock工具的简易实现

先来个总览
在这里插入图片描述
MockTool:主入口

public class MockTool {

    private static final MockMethodInterceptor INTERCEPTOR = new MockMethodInterceptor();
    
    public static <T> T mock(Class<T> type) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(type);
        enhancer.setCallbackFilter((method)->method.isBridge() ? 1 : 0);//忽略bridge方法
        enhancer.setCallbacks(new Callback[] {INTERCEPTOR, NoOp.INSTANCE});
        return type.cast(enhancer.create());
    }
	
	public static <T> Stubbing when(T call) {
		return new Stubbing();
	}
}

MockMethodInterceptor:拦截目标方法,记录并返回mock结果

public class MockMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        Invocation invocation = new Invocation(obj.getClass(), method, args);
        Object value = InvocationContainer.findAnswerFor(invocation);
        if (value == null) {
            //查询不到mock结果,视为要打桩
            InvocationContainer.setCurrentInvocation(invocation);
        }
        return value;
    }
    
}

InvocationContainer:记录当前要mock的方法,以及mock结果

public class InvocationContainer {

    private static Invocation currentInvocation;

    private static final ConcurrentHashMap<Invocation, Queue<Object>> INVOCATION_RETURN_MAP = new ConcurrentHashMap<>();

    public static void setCurrentInvocation(Invocation invocation) {
        InvocationContainer.currentInvocation = invocation;
    }

    public static Invocation getCurrentInvocation() {
        return currentInvocation;
    }

    public static void addAnswer(Invocation invocation, Object value) {
        Queue<Object> queue = INVOCATION_RETURN_MAP.get(invocation);
        if (queue == null) {
            queue = new ConcurrentLinkedQueue<>();
            INVOCATION_RETURN_MAP.put(invocation, queue);
        }
        queue.offer(value);
    }

    public static Object findAnswerFor(Invocation invocation) {
        Queue<Object> queue = INVOCATION_RETURN_MAP.get(invocation);
        if (queue == null) {
            return queue;
        }
        return queue.size() > 1 ? queue.poll() : queue.peek();
    }
}

Invocation:抽象对方法的调用

@Setter
@Getter
@AllArgsConstructor
@EqualsAndHashCode
public class Invocation {
    
    private Class<?> mockClass;
    private Method method;
    private Object[] args;
    
}

Stubbing:将mock结果写入mock结果容器

public class Stubbing {

    public Stubbing thenReturn(Object result) {
        if (InvocationContainer.getCurrentInvocation() != null) {
            InvocationContainer.addAnswer(InvocationContainer.getCurrentInvocation(), result);
        } else {
            throw new IllegalStateException("api使用姿势不对");
        }
        return this;
    }
}

最后走一个测试,基本达标。

public class TestMockTool {

    @Test
    public void testMock() {
        ServiceA serviceA = MockTool.mock(ServiceA.class);
        MockTool.when(serviceA.hello("aaa")).thenReturn("111").thenReturn("222");
        System.out.println(serviceA.hello("aaa")); //111
        System.out.println(serviceA.hello("aaa")); //222
        System.out.println(serviceA.hello("bbb")); //null
    }
}
<think>嗯,用户想了解Mockito使用方法和示例代码。首先,我需要回顾Mockito的基本概念,比如模拟对象、桩方法、验证交互等。记得用户可能需要从安装配置开始,所以应该先介绍如何添加Mockito依赖,比如Maven或Gradle的配置。然后,分步骤讲解创建模拟对象的不同方法,比如使用@Mock注解或者Mockito.mock()方法。 接下来是桩方法设置,这部分很重要,用户通常需要知道如何让模拟对象返回特定的值或者抛出异常。要举一些具体的例子,比如when(...).thenReturn(...)的结构,可能还要提到thenThrow和thenAnswer的用法。 然后是验证交互的部分,用户可能想知道如何检查方法是否被调用,调用次数是否正确。需要解释verify的基本用法,以及使用参数匹配器如any()、eq()的注意事项。 还有,用户提到参考了引用中的代码,特别是使用@Mock和@InjectMocks的示例,所以需要详细说明这些注解的使用步骤,包括初始化注解的方式,比如MockitoAnnotations.openMocks(this)或者JUnit的MockitoExtension。 另外,可能需要提到Mockito的一些高级功能,比如参数捕获、部分模拟,或者与Spring框架的集成,但根据用户的问题,可能基础内容更重要。需要确保示例代码正确,语法无误,特别是Java和可能的Kotlin代码,不过用户提供的引用中有Kotlin项目地址,但问题主要是Java的示例,所以重点放在Java上。 最后,生成的相关问题要覆盖用户可能进一步想知道的内容,比如与JUnit5的整合、异常测试、Spring集成、参数捕获等。确保每个步骤都有清晰的说明,代码示例正确,并且引用正确对应的参考来源。</think>### Mockito 使用指南 Mockito 是一个流行的 Java 测试框架,用于模拟依赖对象的行为,简化单元测试。以下是核心使用方法和示例: --- #### 1. **添加依赖** 在项目中引入 Mockito(以 Maven 为例): ```xml <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> ``` 若使用 Kotlin,可参考 `mockito-kotlin` 扩展库[^1]。 --- #### 2. **创建模拟对象** **方式一:通过注解** ```java public class CustomerServiceTest { @Mock private CustomerDao daoMock; // 模拟对象 @InjectMocks private CustomerService service; // 自动注入依赖 @Before public void setUp() { MockitoAnnotations.openMocks(this); // 初始化注解 } } ``` **方式二:通过静态方法** ```java CustomerDao daoMock = Mockito.mock(CustomerDao.class); ``` --- #### 3. **桩方法(Stubbing)** 定义模拟对象的行为: ```java // 当调用 daoMock.findById(1L) 时返回预设对象 Mockito.when(daoMock.findById(1L)) .thenReturn(new Customer("Alice")); // 抛出异常 Mockito.when(daoMock.delete(any())) .thenThrow(new RuntimeException()); // 动态返回 Mockito.when(daoMock.getName()) .thenAnswer(invocation -> "Response"); ``` --- #### 4. **验证交互** 检查方法是否按预期调用: ```java // 验证 daoMock.save() 被调用一次 Mockito.verify(daoMock, times(1)).save(any(Customer.class)); // 验证未调用 Mockito.verify(daoMock, never()).deleteAll(); ``` --- #### 5. **参数匹配器** 灵活匹配输入参数: ```java Mockito.when(daoMock.findByAge(Mockito.anyInt())) // 任意整数 .thenReturn(new Customer()); Mockito.verify(daoMock).update(Mockito.eq("id123"), any()); // 精确匹配第一个参数 ``` --- #### 6. **高级用法示例** **捕获参数值:** ```java ArgumentCaptor<Customer> captor = ArgumentCaptor.forClass(Customer.class); Mockito.verify(daoMock).save(captor.capture()); Customer capturedCustomer = captor.getValue(); ``` **部分模拟(Spy):** ```java List<String> realList = new ArrayList<>(); List<String> spyList = Mockito.spy(realList); // 覆盖部分方法 Mockito.when(spyList.size()).thenReturn(100); ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值