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

徒手撸一个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以及其余(后续会有)东西结合起来的关键。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,105评论 1 32
  • 什么是单元测试? 单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行...
    常晓csc阅读 9,395评论 0 6
  • 自从老夫换了一个新厂之后,单测就写个不停,因为新厂对单测的要求还是比较高的。 在撸单测的过程中,用Mockito,...
    flycash阅读 3,686评论 1 6
  • 单元测试的目标和挑战 单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关...
    jiangmo阅读 2,133评论 0 2
  • Mockito简介什么是mock?在软件开发的世界之外, "mock"一词是指模仿或者效仿。 因此可以将“mock...
    燕京博士阅读 3,552评论 0 6