美文网首页
徒手撸一个Mock框架(四)—— when XXX then 嘿

徒手撸一个Mock框架(四)—— when XXX then 嘿

作者: flycash | 来源:发表于2019-06-12 22:51 被阅读0次

    徒手撸一个Mock框架(一)——如何创建一个mock对象
    徒手撸一个Mock框架(二)——如何创建final类的代理
    徒手撸一个Mock框架(三)—— JUnit4Runner+ClassLoader=为所欲为

    上一篇我们的StupidMock已经解决了创建各种mock对象的问题。今天我们来解决方法调用mock的问题。

    先来看一个例子。在使用Mockito的时候,如果想要mock一个对象的行为,一般的用法是:

    when...thenReturn之后,无论原来的方法原本的实现是什么样子,如果传入a,b两个参数值,那么就会返回固定的Hello

    今天我们要实现的就是这个东西。

    关键点分析

    我们先来思考一下,究竟需要做一些什么。从最抽象的程度来说,一个方法调用可以描述为某个对象调用一个方法,参数是XXX,最后响应是XXX

    所以,我们需要解决四个问题:

    1. 确定对象;
    2. 确定方法;
    3. 方法调用参数;
    4. 返回值;

    如果不考虑用户体验的话,我们可以直接让用户配置一大堆的东西,把我们所需要的信息都配置过来,我们傻瓜式的根据配置跑一下就可以了。

    但是在考虑了用户体验的时候,就不能这么做了。

    所以,前三个问题也成了很大的问题。在前面的例子里,when接收的是doSomething调用之后的返回值,所以肯定不能在when方法里面获取到对象和调用方法的信息。于是我们的选择就只剩下了在调用doSomething的时候将内容保存下来。

    而在thenReturn的时候,这个return的内容,就直接是受到我们控制的,所以很好解决,直接在StupidMock里面保存起来就可以。

    于是我们要做的事情就是:

    1. mock对象调用某个方法的时候,保存下这次调用的对象,参数信息,以及方法;
    2. 当调用thenReturn的时候,将参数也保存下来;
    3. 将前面保存的信息关联起来,放到一起。

    最终保存的东西,我们称为stub

    所以当用户发起一次真的调用的时候,我们要做的就是,从所有创建的stub里面,找到匹配的那个,将stub中设置的返回值返回。

    获取对象、方法和参数

    前面的分析里面提到,我们只能在doSomething方法里面收集对象、方法和参数。

    现在我们要考虑的问题是:

    1. 如何收集;
    2. 放在哪里,怎么获取;

    第一个问题理论上来说,并不复杂,因为我们创建的mock对象,是利用cglib来创建的,我们可以在创建代理的时候,传入callback参数,这个callback就是用来保存这一次的调用对象、方法和参数;

    第二个问题,更加多的是设计的问题。我们可以直接把这些信息放在mock对象内部,然后在when方法里面将它取出来。有一个问题是,我们无法区别两种调用,即无法区别用户是在创建一个stub还是真的在执行一个调用。

    解决办法就是,我们都处理。既认为这是一次调用,也认为这是一个创建stub。

    1. 作为一次调用,我们将从所有已经注册的stub里面找到匹配的,返回注册的返回值;
    2. 作为一次创建stub的步骤之一,我们将保存这次调用的上下文;

    为了统一处理,我们会在创建mock对象的时候,加入一个默认的stub,该stub就是各种类型的默认值。如基本类型则是基本类型对应的默认值,如果是对象则返回null

    Callback实现

    上图是我们的mock对象时候使用的方法,很容易发现,关键点就在于实现MethodInterceptor接口,并且注册进去。

    所以我们先实现一个自己的MethodInterceptor

    MethodInterceptor的实现关键是MockObjectSkeletonThreadSafeStubBuilder。本质上来说,这个实现只是一个“胶合层”,负责将StupidMockcglib粘在一起。虽然理论上来说,我可以将MockObjectSkeletonThreadSafeBuilder的逻辑都直接写在其中,但是这会让我们的实现过于臃肿。

    这里还有一个将Object转化为ArgMatcher的过程。这是因为,在我们的stub里面,并不能直接使用这个参数,而是要保存一些参数匹配条件。

    比如说有些时候我们的写法可能是:when(obj.doSomething(any(),any()).then(...)

    于是我们对应的StupidMock就变成了:

    最终的使用效果类似:

    MockObjectSkeleton

    MockObjectSkeleton在这里更加接近一个容器的概念。它里面负责放置stub实例,并且从stub里面找出一个来,执行stub,并返回对象。

    这里有一个地方需要注意的是,我采用的是一个List来保存stub。并且每次添加的时候都是将stub加在队列前。

    这是一个非常粗糙的做法:

    1. 按照我们的匹配原则,如果我们设定了两个stub,对于某一次方法调用,那么后一个设定的stub就会覆盖掉前一个,作为结果返回;
    2. 对于一个方法来说,可以有很多stub,并且我们没有提供删除某些stub的方法;

    可以考虑用一个Map结构来取代List,以实现单个方法只会有一个stub

    StubBuilder

    StubBuilder则是另外一个关键点。上图的接口定义其实很好理解,需要额外解释的就是addOberver方法。

    这是一个观察者模式的应用。它主要是为了解决MockObjectSkeleton需要维护stub,而创建stub则是在StubBuilder里面完成的。除此以外,一种可取得做法我们可以将MockObjectSkeleton的实例传入StubBuilder实例,但是这意味着两者将强耦合在一起,这是我所不希望的。所以设计了一个BuildingStubObserver接口,单纯就是为了解耦,以及扩展性。

    现在要来看最为绕的地方了,就是ThreadSafeStubBuilder。在此之前,我要先分析一下我们面对的困难时什么。

    在我们的模型里面,牵涉到了simpleObject——即mock对象,StupidMockMethodInterceptorAdaptorImpl实例——在创建mock对象的时候创建,StupidMock——它的静态方法,还有核心stub实例。

    这意味着,我们需要在这些所有牵涉到的对象或者类中共享stub实例的创建过程。我们要在StupidMockMethodInterceptorAdaptorImpl里面创建StubBuilder并且这个StubBuilder要在StupidMock里面被返回。

    于是关键问题是,StupidMockMethodInterceptorAdaptorImpl怎么把StubBuilder传递给StupidMock

    答案是通过某个共享的中间变量。

    这个共享中间变量就是ThreadSafeStubBuilder

    实际上,我们是利用了ThreadSafeStubBuilder里面的静态变量stubBuilder来实现这种共享的。stubBuilder利用了Java的ThreadLocal特性,来保证线程安全。

    所以,无论是在StupidMockMethodInterceptorAdaptorImpl里面new ThreadSafeStubBuilder还是在StupidMock里面new ThreadSafeStubBuilder,它们实际上操作的都是同一个StubBuilder

    IStub, Answer和ArgMatcher

    IStub的定义只有两个方法,一个是判断自身与某一次实际调用是否匹配,如果匹配的话,则意味着要使用该stub实例,于是调用getAnswer得到answer实例.

    Answer接口被定义为函数式接口,里面只有一个方法。它代表的就是用户想要在实际调用时候mock的动作。

    IStub的默认实现DefaultStubImpl之中,match方法的实现如下:

    其逻辑最重要的部分就是参数匹配,这是利用ArgMatcher来进行的:

    注意到的是,在StupidMockMethodInterceptorAdaptorImpl里面我们只使用了一种实现,就是FixedValueArgMatcherImpl。因为这一篇文章不讨论复杂的参数匹配问题,我会在下一篇讨论这个问题。FixedValueArgMatcherImpl就是指匹配特定值,其实现是:

    设计总结

    这一篇文章,其实没有涉及太多复杂的技术,更加多的是设计上的问题。我在弄这个东西的时候,很多时候都抄袭了Mockito的东西,不过将里面复杂的东西都去掉了。

    但是核心问题,或者说,关键点,我自认为还是保留下来了。

    这个核心问题就是我所谈及的,如果让StubBuilder在各个地方共享,并且能够保证线程安全,以及mock的正确性。

    现在我来列举一下这个设计的核心接口。这些接口定义了整个系统的运作方式,堪称灵魂。

    第一个接口是IStub接口。定义了一个stub应该知晓自己是否能够被某次调用所使用,并且定义了该如何“应答”这次调用。

    这就是Answer接口。Answer接口解决了在mock中做什么的问题。在Mockito里面那些复杂then, thenReturn, thenThrow之类的,都可以实现Answer接口以达成。

    而另外一个接口StubBuilder接口,则定义了一个stub该如何被创建出来。它是将cglibmock对象,和StupidMock以及其余(后续会有)东西结合起来的关键。

    至于剩下的东西,不过是一些边角之物。

    相关文章

      网友评论

          本文标题:徒手撸一个Mock框架(四)—— when XXX then 嘿

          本文链接:https://www.haomeiwen.com/subject/qbcdfctx.html