2008年12月19日星期五

如何正确的使用Mock

首先我不是反Mock者,但确实对使用Mock持比较审慎的态度,因为Mock是非常难于正确使用的, mock最常见的问题在于假设!假设!假设!

有这样一个功能,当有工作的时候,公民需要买需要买医疗保险,住房公积金和养老保险,如果失业了他只需要买养老保险:


public void requreInsurance(Insurances insurances) {
if (people.getJob() == null) {
insurances.add(new RetirementInsurance());
} else {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}


相应的Mock测试有两种情况需要覆盖:

* mock people对象, 假设getJob方法返回null,验证insurances中只有包括养老保险
* mock people对象, 假设getJob方法返回Not null,验证insurances中包含四金


在这里的Mock测试进行了假设,它的coorelation people对象在有工作的时候返回非Null的Job对象,而在没有工作的时候返回Null,类似的代码在任何一个项目中都可以找得到踪迹。

问题在于这样的假设可以被悄无声息的破坏掉,假如有人重构了People对象, 在没有工作时返回一个new NullJob()对象(Null Object Pattern), 这样重构后,失业的人也不得不买四金了,然而我们之前编写的mock测试会100%的通过

如何解决这样的问题呢?

功能测试是一个解决思路,因为在Mock测试中,我们不断的在层与层之间做出假设,一定需要一个端到端的测试来验证我们的假设是否正确。功能测试解决了部分的问题, 回顾上面的问题,你发现至少需要编写两个功能测试才能百分之百的发现刚才重构引入的bug, 如果只编写了happen path的功能测试(有People工作的功能测试),那你只有祈祷QA能帮你及时的找到问题了。 不幸的是,由于功能测试的代价比较大,所以大多数的人都只会编写有限的功能测试,往往这些测试仅仅用于覆盖Happy path. 对于如此简单的问题功能测试尚且不能解决问题,更遑论我们“大型企业级超复杂”的项目呢。

另一个方法就是减少假设,回顾一下我们的实现代码:

if (people.getJob() == null) {
....
} else {
.....
}


getJob的返回值是我们需要进行两次假设的根源,如果没有返回会怎样?


public void requreInsurance(Insurances insurances) {
people.requreInsurance(insurances);
}

public clas People {
public void requreInsurance(Insurances insurances) {
job.requreInsurance(insurances);
}
}

public class Job {
public void requreInsurance(Insurances insurances) {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}

public class NullJob {
public void requreInsurance(Insurances insurances) {
insurances.add(new RetirementInsurance());
}
}

在第一个例子中我们需要进行两个假设(Null 和 Not Null),而第二个例子,我们只需要一种假设

第一个例子中稀松平常的代码违反了基本的面向对象的原则“封装”,简而言之是tell do not ask!!原则,因为违反了这个原则,我们不得不做出很多假设,减少假设的一个有效途径就是减少return,tell你的对象替你效劳,如果你对写不出健壮的mock测试烦恼,不妨看看是否写出了符合面向对象原则的产品代码,

正确使用Mock的原则就是尽量不用,改善设计才是王道,代码难于使用通常的方法进行测试而不得不使用mock,绝对是一种smell,不要使用mock来掩盖这种味道。

最后推荐李晓同学的

不要把Mock当作你的设计利器


还有感谢Chris Stevenson今天的Session和帮助。

3 条评论:

taowen 说...

所有的原则都是由一个原则推导来的,那就是DRY。Mock的问题在于它是真实实现的重复。重复一次可以忍受,每个地方都要重复那就不能忍受了。我用Mock,但是不用mock框架。因为mock框架让你很容易就把一个实现重复“实现”很多遍。手写mock,像cotta framework那样。

MRZ 说...

没错,double implementation也是mock的一个坏味道,在Mock Roles not Objects这篇论文里面说的,编写mock框架的主要意图在于通过对象的行为确定它的类型,它更像是一个用于设计的框架(TDD)。但是mock对象有一点好处,它会让烂代码闻起来更刺鼻(使用mock对于某个方法进行测试时,要mock多个对象,设置expectation的语句远比真正的测试代码长)

MRZ 说...

mock中还有很多别的坏味道,比如从mock对象的方法调用中返回mock过的对象