- 浏览: 9120 次
文章分类
最新评论
Martin Fowler大师的一篇文章
原文地址:
http://martinfowler.com/articles/mocksArentStubs.html
主要是用于个人的学习与理解,许多地方翻译得不太准确:
术语"Mock Object"已经成为流行词,它的目的是在测试中用来模拟真实的对象。现在在许多的语言环境中,都有相应的框架能够轻松的创建mock对象。然而,人们并没有意识到,mock是一种特殊形式的测试类,并且可以实现不同分隔的测试。在本文中,我将要解释mock对象是怎样工作的,他们怎样支撑基于行为验证的测试方法,并且在mock社区中是怎样使用它来进行不同风格的测试的。
我第一次遇到“mock object”术语是几年前在XP社区。自动那时候起,我越来越多次的遇到了mock对象。部分是因为许多使用mock object的高手都是我在ThoughtWork的同事。同时部分原因是因为我越来越多的在收XP影响的测试文献中看到了它们。
但是我经常性的看到mock object并没有得到很好的描述。特别是我经常看到mock object跟stub混为一谈,都被认为是一种测试中的通用助手(me:不太准确)。我理解这样的混淆,我有一段时间也认为它们很相像。但是通过与mock开发者的一些交谈,让我对mock有了一些理解。
mock与stub的不同点实际上可以说它们俩是完全不同的两个事物。一方面,它们对于怎样去确定测试的结果所使用的方法是不相同的,区别在于,一个使用状态确认(state verification)的方法而另外一个使用的是行为确认(behavior verification)。从另一方面来说,就是对于一种将测试与设计结合在一起的方法的完全不同的理念。在这里我称呼他们为classic形式和mockist形式的测试驱动开发(TDD)。
在本文的早期的版本中,我已经意识到他们之间是有区别的,但是我将这两种区别混合在一起了。但是在那以后,我对他们的理解又加深了。结果就导致了这篇文章的更新。如果你之前没有读过本文,那么你可以忽略我成长的痛苦,我已经重新写了这篇文章,完全覆盖了之前的版本。但是如果你对之前的版本比较熟悉,那么我已经将原来的二分法表示基于状态的测试和基于交互的测试转变为了状态/行为验证的测试方法和classic/mockist的测试驱动开发分法。我已经调整了我的“词汇表(me:这个很重要,是交流的通用语言)”与Gerard Meszaros's xUnit patterns book相符合。
正常的测试
我通过一个简单的例子来开始这两种风格的讨论。(用例使用的是java语言,但是原则适用于所有的面向对象语言),我们想去的一个订单(Order)对象,并且通过一个仓库(warehouse)对象来填充它。order对象非常简单,只有产品和数量两个信息。仓库对象中持有不同的产品的目录。当我们让一个仓库去填充订单的时候,会得到两种可能的回应。如果仓库中有足够的产品去填充订单对象,订单对象就会被填充,而且参数中相应产品的数量就会降低到对应的数额。如果仓库中没有足够的产品,那么订单对象就不会被填充,且仓库没有任何的变化。
这这个行为暗示了一对测试,看起来像是相当普通的Junit测试。
public class OrderStateTester extends TestCase{
private static String TALISKER = "Talisker";
private static String HIGHLAND_PARK = "Highland Park";
private WareHouse warehouse = new WareHouseImpl();
protected void setup() throws Exception{
warehouse.add(TALISKER , 50);
warehouse.add(HIGHLAND_PARK , 25);
}
public void testOrderIsFilledIfEnoughInWarehouse()
{
Order order = new Order(TALISKER , 50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0 , warehouse.getInventory(TALISKER));
}
public void testOrderDoseNotRemoveIfNotEnough() {
Order order = new Order(TALISKER , 51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50 , warehouse.getInventory(TALISKER));
}
}
xUnit测试遵循了典型了四步测试顺序:准备(setup),执行(exercise),验证(verify),拆除(teardown)。在这个案例中,setup的步骤是分为两部分的,一部分是在setup方法中进行的(setup the WareHouse),另一部分是在测试方法中进行的(setup the Order)。对于order.fill的调用是执行的部分。这就是我们需要测试的对象执行的地方。assert语句是接下来的验证阶段,验证被测试的方法是否正确的完成了它的任务。在本案例中,没有显式的卸载步骤,垃圾收集机制为我们隐式的做了这一步。
在测试期间,有两个对象被我们放到了一起,order对象是我们要测试的对象,但是为了Order.fill能够工作,我们还需要Warehouse对象。在这种情况下,Order对象是我们在测试中聚焦的对象。面向测试的人喜欢用术语“object-under-test”或者"systen-under-test"来称呼它。这两个术语都不太好称呼,但是它们是被广泛接受的术语,所以将捏住我的鼻子,并且使用它。跟Meszaros一样我将使用数据“System Under Test”,或者说缩略词“SUT”。
所以为了这个测试,我需要SUT(Order)和协作者collaborator(warehouse)。我有两个理由需要warehouse:第一个就是需要通过它来配合测试的运行(因为Order.fill调用了warehouse的方法),第二个就是我需要它来进行验证(因为Order.fill产生的一个结果就是潜在的改变了warehouse的状态)。当我们更深入的探讨这个主题的时候,你会发现在SUT于collaborator之间有很多的区别。(在本文的早起版本中,我把SUT称作主要对象,把collaborator称作次要对象)
这种测试风格使用的是“状态确认”的风格:意思就是我们确认被执行的方法是否能够正确的工作,是在执行完成以后通过验证SUT及其collaborator的状态来决定的。我们将会看到,mock对象使用了一个不同的途径去进行了验证。
通过Mock对象进行测试
现在我们将要讨论同样的行为并且使用mock对象。在这段代码中,我将使用jMock来定义mock对象。jMock是一个java mock对象库。当然还有其他的mock对象库,但是jMock是这项技术的发起人写的最新的库,所以用它作为开始很合适。
public class OrderInteractionTester extends MockObjectTestCase{
private static String TALISKER = "Talisker";
public void testFillingRemovesIfInStock() {
Order order = new Order(TALISKER , 50);
Mock warehouseMock = new Mock(WareHouse.class);
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER) , eq(50)).will(returnValue(true));
warehouseMock.expects(once()).method(remove).with(eq(TALISKER) ,eq(50))
.after("hasInventory");
order.fill((Warehouse)warehouseMock.proxy());
warehouseMock.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock {
Order order = new Order(TALISKER , 50);
Mock warehouse = mock(Warehouse.class);
warehouse.expects(once()).method("hasInventory")
.withAnyArguments().with(returnValue(false));
order.fill((Warehouse)warehouse.proxy());
assertFalse(order.isFilled());
}
}
先将注意力集中在测试方法testFillingRemovesIfInStock上面,as I've taken a couple of shortcuts with the later test(此句未读懂).
首先,setup步骤就不一样。首先,它被分成了两步:数据和预期。在数据步骤,准备了我们需要工作的对象,从这个意义上来看它与传统的setup步骤是相似的。不同的是我们创建的对象。SUT对象是相同的(一个order),然而collaborator并不是一个warehouse对象,取而代之的是一个mock后的warehouse对象,从技术上来说就是Mock类的一个对象。
setup的第二步在mock对象上面创建预期。这些预期显示了mock对象的哪些方法会在测试SUT是的时候被执行。
在执行完成以后,我就要做验证的工作,包含两个方面。我跑了SUT的断言,跟之前一样。然而,我同样验证了mock对象,验证他们是否像所期望的那样去运行。
在这里关键的不同点就是我们怎样去验证order对象在与warehouse交互的过程中做了正确的事情。借助于状态验证的方法,我们通过warehouse的状态去断言。mocks使用的是行为验证的方法,取而代之的是我们验证了order是否正确的调用了warehouse。我们通过在setup阶段中告诉mock对象我们的期望,并且在verification阶段通知mock对象自己去验证。只有order对象是使用断言去检查的。如果方法没有改变order对象的状态,那么根本就不会有断言。
在第二个测试中我做了不同的事情。首先我使用了不同的方法去创建了mock对象,使用MockObjectTestCase的mock方法而不是构造器。这事JMock库中的一个便利方法,意味着我不需要在随后的步骤中显式的去验证。所有的通过这个便利方法创建的mock对象都会在测试结束以后进行自动的验证。我本来同样能够在第一个测试中这样做,但是我想更显式的表示这个验证以用来显式用mock进行测试是怎样工作的。
在第二个测试用例中的第二个不同点就是我通过使用withAnyArguments放松了期望中的约束。原因是在第一个测试中,传入到warehouse中参数的个数已经被验证过了,所以在第二个测试中就不需要重复了。如果以后order的逻辑需要修改,那么就只有一个测试用例会失败,减轻了迁移测试的压力。结果就是我可以完全的忽略withAnyArgument,就当它是默认的。
使用EasyMock
同样有其他的一些mock对象库。我遇到过EasyMock很多次,不管是在它的java版本还是.net版本。EasyMock同样可以使用行为验证,但是在形式上与jMock有一些区别。下面同样是一个类似的测试:
public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";
private MockControl warehouseControl;
private Warehouse warehouseMock;
public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehoueMock = (Warehouse)watehouseControl.getMock();
}
public void testFillingRemovesInventoryIfInStock() {
Order order = new Order(TALISKER , 50);
warehouseMock.hasInventory(TALISKER , 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER , 50);
warehouseControl.replay();
order.fill(warehouseMock);
warehouseControl.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER , 51);
warehouseMock.hasInventory(TALISKER , 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();
order.fill(order.isFilled());
warehouseControl.verify();
}
}
EasyMock使用一组 (记录/重放)record/replay的隐喻来设置期望。对于每一个你想mock的对象,你都需要创建一个control和一个mock对象。mock对象满足了次要对象的接口(me:这边没改过来?上面已经提过了这个secondary object 应该是叫做collaborator),control给了你额外的特性。为了表示期望,你调用了一个方法,传入你希望在mock中出现的参数。接着如果你先要一个返回值,那么你就调用control的setReturnValue方法。一旦你完成了对于期望的设置,你调用了control的replay方法。到这个点为止,mock对象完成了recording的步骤,并且做好了响应主要对象(me:SUT)的准备。一旦完成以后,你就可以调用control的verify方法了。
看起来人们经常在第一次遇到record/replay隐喻的时候,就能够快速的使用他们。相比于jMock的约束来说,它有一个优势就是你在真正的调用mock对象的方法,而不是讲方法当做字符串。这意味着你可以使用IDE的代码补全,并且任何对于方法名称的重构都会自动的去更新这些测试。而缺点就是你不能有关于失败的约束。(the looser constraint)
jMock的开发者正在进行一个在新版本的开发,其中会使用其他的技术让你能够实际的去进行方法的调用。
Mock与Stub的区别
当我们第一次介绍的时候,许多人很容易将mock objects与使用stub的一般概念混淆。从那以后人们对于区别有了更好的理解。(并且我希望这是因为我这篇文章前一个版本的帮助)。然而要完全的了解人们使用mock的方法,则理解mocks和其他种类的doubles很重要。
("doubles"?不要担心这是一个新的术语,等过一些篇幅就会清晰)
当你像这样做测试的时候,你就会一次聚焦于软件的一个元素,UT。问题是要进行一个单一的单元测试,你经常需要其他单元的配合。就像是我们例子里面的warehouse。
在我上面展示的两种风格的测试中,第一个case中使用了一个真实的warehouse对象,在第二个case中使用了mock warehouse,当然就不是一个真实的warehouse对象。使用mocks是一种在测试中不使用真实warehouse的途径,但是在测试中其他一些形式像这样不使用真实的对象:
一会儿用于讨论的词汇表将会变得比较混乱。各种各样的词语都会使用到:stub,mock,fake,dummy.在本文中我想遵循Gerard Meszaros的书中的词汇表。这是不是所有人都在使用的,但是我想这是一个比较好的词汇表并且因为这是我的文章,所以我使用这些词汇。
Meszaros使用术语“Test Double”作为所有的在测试中伪装成真实的对象的对象的通用术语。这个名字来自于电影中Stunt Double的概念(他其中的一个目的是避免使用一个已经被广泛使用过的单词)。然后Meszaros定义了四种类型的double:
Dummy对象是被到处传递当时从未真正被使用过得对象。它们经常只是被用来填充参数列表。
Fake对象实际上是有工作实现的(working implementations),但是进场会走一些捷径使得它们并不适合产品。(一个内存数据库就是一个很好的例子)
Stub在测试调用的过程中只提供一个封闭式的回答,通常对测试中其他的方面完全没有反应。stub同时还可以记录关于调用的一些信息,例如email gateway stub可以记住它发送出去的message,或者只是记录它发送了多少条message。
mocks是我们现在要讨论的,是一个预先写好的对象,它的预期(exepectations)展现了调用者想要的反馈。
在这些double中,只有mocks是坚持使用行为验证的。而其他的double经常使用的是状态验证。在exercise阶段mock对象的行为和其他的double对象是一样的,因为他们都要使得sut相信他们是在于真实的collaborator进行交互。但是mock在setup和verifacation步骤中是不一样的。
想要更多的去探索test doubles,我们需要扩展一下我们的例子。许多人只会在真实的对象使用起来比较不合适的时候才会去使用一个test double。一个更加常见的使用test double的例子是:如果我们在填充订单失败的时候,需要发送一个email信息。但是问题是我们并不想在测试期间真正的给用户发送信息。所以替代它的就是我们使用了一个email系统的test double,这样我们就可以进行控制和操纵。
现在我们可以开始来看看mock与stub之间的区别。如果我们要给发送mail的行为做一个测试,我们可以像下面这样,写一个简单的stub:
public interface MailService(){
public void send(Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send(Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
我们可以像下面这样在stub上使用状态验证的测试方法:
public class OrserStateTester {
Order order = new Order(TALISKER , 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1 , mailer.numberSent());
}
当然这是一个非常简单的测试,只会发送一条message。我们还没有测试它是否会发送给正确的人员或者内容是否正确,但是接下来它将会展示这一点:
如果使用mock,那么写个测试看起来就不太一样了:
class OrderInteractionTester...
public void testOrderSendsMailIfUnFilled() {
Order order = new Order(TALISKER , 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService)mailer.proxy());
order.expects(once()).method("hasInventory").withAnyArgument()
.will(returnValue(false));
order.fill((Warehouse)warehouse.proxy())
}
}
在两个case中我都使用了test double来代替真实的mail service。不同的是,stub使用的是状态确认的方法,而mock使用的是行为确认的方法。
想要在stub中使用状态确认,我需要在stub中增加额外的方法用来协助验证。因此stub实现了MailService但是增加的额外的测试方法。
mock对象一直都是使用行为验证的方法,其实stub同样也可以。Meszaros 将使用行为验证的stub称呼为Test Spy。区别在于double怎样正确的运行与验证。并且我将留给你自己去探索。
classic and mockist testing
在这个时候,我可以开始我对于第二个二分法的探索了,也就是classic TDD 与 mockist TDD。在这里有一个重大的议题就是什么时候去使用一个mock(或者其他的stub)
classic风格的TDD是尽可能的使用真实的对象,并且在真实的对象不适合使用的时候才会去使用double。所以一个clssic Tester 会使用一个真实的warehouse对象和一个mail service的double对象。而对于double的类型不是很重要。
一个mock TDD的从业者,对于任何对象的感兴趣的行为都会使用mock。在本案例中就是warehouse和mailService。
尽管各种各样的mock框架是为mockist testing而设计的,但是许多的classicist发现它们在创建double的时候会很有用。
mockist风格的一个重要的分支就是行为驱动开发(Behavior Driven Development)(BDD)。BDD一开始是有我的同时Dan North开发出来的,目的是通过聚焦于TDD作为一种设计技术是怎样操作的来帮助人们学习TDD。这导致了将测试通过行为来重命名来探索TDD能够帮助思考对象需要做什么。BDD使用了mockist的途径,但是它对其进行了扩展。我不想在这里深入下去,其与本篇文章的唯一关联就是要说明BDD是试图使用mockist testing进行TDD的一个变种。我把它留给你以根据这个链接来找到更多的信息。
Choosing Between the Differents
在本文中我已经解释了一对不同之处:基于状态的验证和基于行为的验证与classic TDD 和mockist TDD。当在它们之间进行选择的时候,我需要参考哪些信息呢?我将首先开始状态验证与行为验证之间的选择:
第一个要考虑的事情就是上下文(Context),我们正在考虑的是一个简单的协作者(collaborartion)就像order和warehouse,还是一个比较麻烦的就像order和mailService。
如果是一个简单的协作者(collaborator)那么选择就比较容易了。如果我是一个classic TDDer,那么我就不会使用mock,stub或者其他任何类型的double。我使用一个真实的对象并且使用基于状态的验证。如果我是一个mockist TDDer,那么我会选择使用mock和基于行为的验证。根本不需要抉择。
如果是一个麻烦的协作者(collaborator),如果我是一个mockist那就没有选择了,我只会使用mocks和基于行为的验证。如果我是一个classicist,那么我确实有选择,但是那个对于每个人来说都不是重大的抉择。一般来说classicist会选择以一个case一个case为基础,选择对于每种情况来说最容易的路线。
所以就像我们所看到的,基于状态验证和基于行为的验证并不是一个大的抉择。真正的议题是在classic TDD与mockist TDD之间。因为基于状态验证的特性和基于行为验证的特性确实会影响这个议题,而且那也是我最多聚焦的地方。
当时在开始之前,让我先抛出一个极端(边缘)的情况。偶尔你正在进行的事情确实难以通过状态来验证,即使他们不是一个麻烦的协作者(collaborator)。一个突出的例子就是缓存(cache)。最主要的一点就是,在cache中你不能通过缓存的状态来判断其是否命中。这就是一个即使对于一个最坚持的classic TDDer来说也应该广泛使用基于行为的验证的例子。我相信在每一个方向上面的选择都会有一些意外情况发生。
当我们要在classic TDD 与mockist TDD之间做出抉择的时候,有许多的因素需要考虑。所以我粗略的将它们分成了几组:
Driving TDD
Mock Object脱胎于xp社区,而且xp其中的一个实践特性就是其着重于测试驱动开发--一个系统的设计是不断的通过写测试来迭代式的进化的。
因此对于mockist特别的讨论mockist testing对于设计的影响就不奇怪了。特别是他们提倡了一种风格称作为需求驱动开发(need-driven development)。如果使用这种风格,在开始开发一个story的时候,你会先测试你的系统的外部。用一些接口去对象化你的SUT和它的邻居。特别是设计SUT的外围接口。
一旦你的第一个测试运行起来以后,测试中的期望就会为下一步的动作提供一个规格知道,并且同时是下一个测试的起点。你将每一个期望都变成对于协作者的测试。并且重复这样的工作使得系统在同一时间里面只有一个SUT。这种风格也被称为由外而内(outside-in),是一个非常有描述性的名字。它能够在分层系统下面工作得很好。你第一次开始于开发UI层,并且使用对于下面的层使用了mock。然后写下面一层的测试,逐步工作通过一次只处理系统的一个层。这是一个非常结构化的和可控的途径,并且许多人相信它能够帮助指导OO和TDD的初学者。
classic TDD并没有提供同样的指导。你一个进行一个类似的一步步进行的方法,使用stub方法来代替mock方法。要做这件事情,不管什么时候你需要从协作者(collaborator)中得到什么,你只要进行硬代码正好能够相应测试所需要的,能够让SUT工作。当你的测试通过了以后,你就需要将硬代码换成合适的代码。
但是classic TDD还能做其他的事情。一个比较常见的风格就是由中间往外部扩展(middle-out)。在这种风格里,当你需要一个特性的时候,你会决定如果要让这个特性工作,你的领域中需要什么。你取得domain来执行你需要的,当他们能够运行了以后,你再去开发顶层的UI层。做这件事情你不需要去伪造任何事物。许多人喜欢这样的风格,因为它能够让注意点一开始就聚焦于领域模型,能够帮助领域逻辑不会泄露到UI层中去。
我需要强调的是,无论mockist还是classist都只会在一段时间内开发一个story。有一个思想流派是一层一层的构建系统,只有当一层完成以后,才会进行下一层的开发。mockist和classicist都有敏捷的背景,而且跟意愿于细粒度的迭代。结果就是他们一个特性一个特性的工作,而不是一层一层的工作。
Fixture Setup(me:没有想到合适的词去翻译)
如果是使用classic TDD,那么你不光要创建SUT,还需要创建SUT所需要的给测试一个相应的协作者(collaborator)。竟然这个例子中只有几个对象,但是在真正的测试中,一般会涉及一个数量庞大的次要对象(协作者Collaborator)。一般来说这些对象在每次测试运行的时候都会被状态与卸载。
然而对于mockist test来说,只需要创建SUT和其最紧密的邻居的mock。它可以在复杂的功能中避免一些工作。(至少在内存中。)
待续,有点头疼。。。
原文地址:
http://martinfowler.com/articles/mocksArentStubs.html
主要是用于个人的学习与理解,许多地方翻译得不太准确:
术语"Mock Object"已经成为流行词,它的目的是在测试中用来模拟真实的对象。现在在许多的语言环境中,都有相应的框架能够轻松的创建mock对象。然而,人们并没有意识到,mock是一种特殊形式的测试类,并且可以实现不同分隔的测试。在本文中,我将要解释mock对象是怎样工作的,他们怎样支撑基于行为验证的测试方法,并且在mock社区中是怎样使用它来进行不同风格的测试的。
我第一次遇到“mock object”术语是几年前在XP社区。自动那时候起,我越来越多次的遇到了mock对象。部分是因为许多使用mock object的高手都是我在ThoughtWork的同事。同时部分原因是因为我越来越多的在收XP影响的测试文献中看到了它们。
但是我经常性的看到mock object并没有得到很好的描述。特别是我经常看到mock object跟stub混为一谈,都被认为是一种测试中的通用助手(me:不太准确)。我理解这样的混淆,我有一段时间也认为它们很相像。但是通过与mock开发者的一些交谈,让我对mock有了一些理解。
mock与stub的不同点实际上可以说它们俩是完全不同的两个事物。一方面,它们对于怎样去确定测试的结果所使用的方法是不相同的,区别在于,一个使用状态确认(state verification)的方法而另外一个使用的是行为确认(behavior verification)。从另一方面来说,就是对于一种将测试与设计结合在一起的方法的完全不同的理念。在这里我称呼他们为classic形式和mockist形式的测试驱动开发(TDD)。
在本文的早期的版本中,我已经意识到他们之间是有区别的,但是我将这两种区别混合在一起了。但是在那以后,我对他们的理解又加深了。结果就导致了这篇文章的更新。如果你之前没有读过本文,那么你可以忽略我成长的痛苦,我已经重新写了这篇文章,完全覆盖了之前的版本。但是如果你对之前的版本比较熟悉,那么我已经将原来的二分法表示基于状态的测试和基于交互的测试转变为了状态/行为验证的测试方法和classic/mockist的测试驱动开发分法。我已经调整了我的“词汇表(me:这个很重要,是交流的通用语言)”与Gerard Meszaros's xUnit patterns book相符合。
正常的测试
我通过一个简单的例子来开始这两种风格的讨论。(用例使用的是java语言,但是原则适用于所有的面向对象语言),我们想去的一个订单(Order)对象,并且通过一个仓库(warehouse)对象来填充它。order对象非常简单,只有产品和数量两个信息。仓库对象中持有不同的产品的目录。当我们让一个仓库去填充订单的时候,会得到两种可能的回应。如果仓库中有足够的产品去填充订单对象,订单对象就会被填充,而且参数中相应产品的数量就会降低到对应的数额。如果仓库中没有足够的产品,那么订单对象就不会被填充,且仓库没有任何的变化。
这这个行为暗示了一对测试,看起来像是相当普通的Junit测试。
public class OrderStateTester extends TestCase{
private static String TALISKER = "Talisker";
private static String HIGHLAND_PARK = "Highland Park";
private WareHouse warehouse = new WareHouseImpl();
protected void setup() throws Exception{
warehouse.add(TALISKER , 50);
warehouse.add(HIGHLAND_PARK , 25);
}
public void testOrderIsFilledIfEnoughInWarehouse()
{
Order order = new Order(TALISKER , 50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0 , warehouse.getInventory(TALISKER));
}
public void testOrderDoseNotRemoveIfNotEnough() {
Order order = new Order(TALISKER , 51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50 , warehouse.getInventory(TALISKER));
}
}
xUnit测试遵循了典型了四步测试顺序:准备(setup),执行(exercise),验证(verify),拆除(teardown)。在这个案例中,setup的步骤是分为两部分的,一部分是在setup方法中进行的(setup the WareHouse),另一部分是在测试方法中进行的(setup the Order)。对于order.fill的调用是执行的部分。这就是我们需要测试的对象执行的地方。assert语句是接下来的验证阶段,验证被测试的方法是否正确的完成了它的任务。在本案例中,没有显式的卸载步骤,垃圾收集机制为我们隐式的做了这一步。
在测试期间,有两个对象被我们放到了一起,order对象是我们要测试的对象,但是为了Order.fill能够工作,我们还需要Warehouse对象。在这种情况下,Order对象是我们在测试中聚焦的对象。面向测试的人喜欢用术语“object-under-test”或者"systen-under-test"来称呼它。这两个术语都不太好称呼,但是它们是被广泛接受的术语,所以将捏住我的鼻子,并且使用它。跟Meszaros一样我将使用数据“System Under Test”,或者说缩略词“SUT”。
所以为了这个测试,我需要SUT(Order)和协作者collaborator(warehouse)。我有两个理由需要warehouse:第一个就是需要通过它来配合测试的运行(因为Order.fill调用了warehouse的方法),第二个就是我需要它来进行验证(因为Order.fill产生的一个结果就是潜在的改变了warehouse的状态)。当我们更深入的探讨这个主题的时候,你会发现在SUT于collaborator之间有很多的区别。(在本文的早起版本中,我把SUT称作主要对象,把collaborator称作次要对象)
这种测试风格使用的是“状态确认”的风格:意思就是我们确认被执行的方法是否能够正确的工作,是在执行完成以后通过验证SUT及其collaborator的状态来决定的。我们将会看到,mock对象使用了一个不同的途径去进行了验证。
通过Mock对象进行测试
现在我们将要讨论同样的行为并且使用mock对象。在这段代码中,我将使用jMock来定义mock对象。jMock是一个java mock对象库。当然还有其他的mock对象库,但是jMock是这项技术的发起人写的最新的库,所以用它作为开始很合适。
public class OrderInteractionTester extends MockObjectTestCase{
private static String TALISKER = "Talisker";
public void testFillingRemovesIfInStock() {
Order order = new Order(TALISKER , 50);
Mock warehouseMock = new Mock(WareHouse.class);
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER) , eq(50)).will(returnValue(true));
warehouseMock.expects(once()).method(remove).with(eq(TALISKER) ,eq(50))
.after("hasInventory");
order.fill((Warehouse)warehouseMock.proxy());
warehouseMock.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock {
Order order = new Order(TALISKER , 50);
Mock warehouse = mock(Warehouse.class);
warehouse.expects(once()).method("hasInventory")
.withAnyArguments().with(returnValue(false));
order.fill((Warehouse)warehouse.proxy());
assertFalse(order.isFilled());
}
}
先将注意力集中在测试方法testFillingRemovesIfInStock上面,as I've taken a couple of shortcuts with the later test(此句未读懂).
首先,setup步骤就不一样。首先,它被分成了两步:数据和预期。在数据步骤,准备了我们需要工作的对象,从这个意义上来看它与传统的setup步骤是相似的。不同的是我们创建的对象。SUT对象是相同的(一个order),然而collaborator并不是一个warehouse对象,取而代之的是一个mock后的warehouse对象,从技术上来说就是Mock类的一个对象。
setup的第二步在mock对象上面创建预期。这些预期显示了mock对象的哪些方法会在测试SUT是的时候被执行。
在执行完成以后,我就要做验证的工作,包含两个方面。我跑了SUT的断言,跟之前一样。然而,我同样验证了mock对象,验证他们是否像所期望的那样去运行。
在这里关键的不同点就是我们怎样去验证order对象在与warehouse交互的过程中做了正确的事情。借助于状态验证的方法,我们通过warehouse的状态去断言。mocks使用的是行为验证的方法,取而代之的是我们验证了order是否正确的调用了warehouse。我们通过在setup阶段中告诉mock对象我们的期望,并且在verification阶段通知mock对象自己去验证。只有order对象是使用断言去检查的。如果方法没有改变order对象的状态,那么根本就不会有断言。
在第二个测试中我做了不同的事情。首先我使用了不同的方法去创建了mock对象,使用MockObjectTestCase的mock方法而不是构造器。这事JMock库中的一个便利方法,意味着我不需要在随后的步骤中显式的去验证。所有的通过这个便利方法创建的mock对象都会在测试结束以后进行自动的验证。我本来同样能够在第一个测试中这样做,但是我想更显式的表示这个验证以用来显式用mock进行测试是怎样工作的。
在第二个测试用例中的第二个不同点就是我通过使用withAnyArguments放松了期望中的约束。原因是在第一个测试中,传入到warehouse中参数的个数已经被验证过了,所以在第二个测试中就不需要重复了。如果以后order的逻辑需要修改,那么就只有一个测试用例会失败,减轻了迁移测试的压力。结果就是我可以完全的忽略withAnyArgument,就当它是默认的。
使用EasyMock
同样有其他的一些mock对象库。我遇到过EasyMock很多次,不管是在它的java版本还是.net版本。EasyMock同样可以使用行为验证,但是在形式上与jMock有一些区别。下面同样是一个类似的测试:
public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";
private MockControl warehouseControl;
private Warehouse warehouseMock;
public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehoueMock = (Warehouse)watehouseControl.getMock();
}
public void testFillingRemovesInventoryIfInStock() {
Order order = new Order(TALISKER , 50);
warehouseMock.hasInventory(TALISKER , 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER , 50);
warehouseControl.replay();
order.fill(warehouseMock);
warehouseControl.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER , 51);
warehouseMock.hasInventory(TALISKER , 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();
order.fill(order.isFilled());
warehouseControl.verify();
}
}
EasyMock使用一组 (记录/重放)record/replay的隐喻来设置期望。对于每一个你想mock的对象,你都需要创建一个control和一个mock对象。mock对象满足了次要对象的接口(me:这边没改过来?上面已经提过了这个secondary object 应该是叫做collaborator),control给了你额外的特性。为了表示期望,你调用了一个方法,传入你希望在mock中出现的参数。接着如果你先要一个返回值,那么你就调用control的setReturnValue方法。一旦你完成了对于期望的设置,你调用了control的replay方法。到这个点为止,mock对象完成了recording的步骤,并且做好了响应主要对象(me:SUT)的准备。一旦完成以后,你就可以调用control的verify方法了。
看起来人们经常在第一次遇到record/replay隐喻的时候,就能够快速的使用他们。相比于jMock的约束来说,它有一个优势就是你在真正的调用mock对象的方法,而不是讲方法当做字符串。这意味着你可以使用IDE的代码补全,并且任何对于方法名称的重构都会自动的去更新这些测试。而缺点就是你不能有关于失败的约束。(the looser constraint)
jMock的开发者正在进行一个在新版本的开发,其中会使用其他的技术让你能够实际的去进行方法的调用。
Mock与Stub的区别
当我们第一次介绍的时候,许多人很容易将mock objects与使用stub的一般概念混淆。从那以后人们对于区别有了更好的理解。(并且我希望这是因为我这篇文章前一个版本的帮助)。然而要完全的了解人们使用mock的方法,则理解mocks和其他种类的doubles很重要。
("doubles"?不要担心这是一个新的术语,等过一些篇幅就会清晰)
当你像这样做测试的时候,你就会一次聚焦于软件的一个元素,UT。问题是要进行一个单一的单元测试,你经常需要其他单元的配合。就像是我们例子里面的warehouse。
在我上面展示的两种风格的测试中,第一个case中使用了一个真实的warehouse对象,在第二个case中使用了mock warehouse,当然就不是一个真实的warehouse对象。使用mocks是一种在测试中不使用真实warehouse的途径,但是在测试中其他一些形式像这样不使用真实的对象:
一会儿用于讨论的词汇表将会变得比较混乱。各种各样的词语都会使用到:stub,mock,fake,dummy.在本文中我想遵循Gerard Meszaros的书中的词汇表。这是不是所有人都在使用的,但是我想这是一个比较好的词汇表并且因为这是我的文章,所以我使用这些词汇。
Meszaros使用术语“Test Double”作为所有的在测试中伪装成真实的对象的对象的通用术语。这个名字来自于电影中Stunt Double的概念(他其中的一个目的是避免使用一个已经被广泛使用过的单词)。然后Meszaros定义了四种类型的double:
Dummy对象是被到处传递当时从未真正被使用过得对象。它们经常只是被用来填充参数列表。
Fake对象实际上是有工作实现的(working implementations),但是进场会走一些捷径使得它们并不适合产品。(一个内存数据库就是一个很好的例子)
Stub在测试调用的过程中只提供一个封闭式的回答,通常对测试中其他的方面完全没有反应。stub同时还可以记录关于调用的一些信息,例如email gateway stub可以记住它发送出去的message,或者只是记录它发送了多少条message。
mocks是我们现在要讨论的,是一个预先写好的对象,它的预期(exepectations)展现了调用者想要的反馈。
在这些double中,只有mocks是坚持使用行为验证的。而其他的double经常使用的是状态验证。在exercise阶段mock对象的行为和其他的double对象是一样的,因为他们都要使得sut相信他们是在于真实的collaborator进行交互。但是mock在setup和verifacation步骤中是不一样的。
想要更多的去探索test doubles,我们需要扩展一下我们的例子。许多人只会在真实的对象使用起来比较不合适的时候才会去使用一个test double。一个更加常见的使用test double的例子是:如果我们在填充订单失败的时候,需要发送一个email信息。但是问题是我们并不想在测试期间真正的给用户发送信息。所以替代它的就是我们使用了一个email系统的test double,这样我们就可以进行控制和操纵。
现在我们可以开始来看看mock与stub之间的区别。如果我们要给发送mail的行为做一个测试,我们可以像下面这样,写一个简单的stub:
public interface MailService(){
public void send(Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send(Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
我们可以像下面这样在stub上使用状态验证的测试方法:
public class OrserStateTester {
Order order = new Order(TALISKER , 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1 , mailer.numberSent());
}
当然这是一个非常简单的测试,只会发送一条message。我们还没有测试它是否会发送给正确的人员或者内容是否正确,但是接下来它将会展示这一点:
如果使用mock,那么写个测试看起来就不太一样了:
class OrderInteractionTester...
public void testOrderSendsMailIfUnFilled() {
Order order = new Order(TALISKER , 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService)mailer.proxy());
order.expects(once()).method("hasInventory").withAnyArgument()
.will(returnValue(false));
order.fill((Warehouse)warehouse.proxy())
}
}
在两个case中我都使用了test double来代替真实的mail service。不同的是,stub使用的是状态确认的方法,而mock使用的是行为确认的方法。
想要在stub中使用状态确认,我需要在stub中增加额外的方法用来协助验证。因此stub实现了MailService但是增加的额外的测试方法。
mock对象一直都是使用行为验证的方法,其实stub同样也可以。Meszaros 将使用行为验证的stub称呼为Test Spy。区别在于double怎样正确的运行与验证。并且我将留给你自己去探索。
classic and mockist testing
在这个时候,我可以开始我对于第二个二分法的探索了,也就是classic TDD 与 mockist TDD。在这里有一个重大的议题就是什么时候去使用一个mock(或者其他的stub)
classic风格的TDD是尽可能的使用真实的对象,并且在真实的对象不适合使用的时候才会去使用double。所以一个clssic Tester 会使用一个真实的warehouse对象和一个mail service的double对象。而对于double的类型不是很重要。
一个mock TDD的从业者,对于任何对象的感兴趣的行为都会使用mock。在本案例中就是warehouse和mailService。
尽管各种各样的mock框架是为mockist testing而设计的,但是许多的classicist发现它们在创建double的时候会很有用。
mockist风格的一个重要的分支就是行为驱动开发(Behavior Driven Development)(BDD)。BDD一开始是有我的同时Dan North开发出来的,目的是通过聚焦于TDD作为一种设计技术是怎样操作的来帮助人们学习TDD。这导致了将测试通过行为来重命名来探索TDD能够帮助思考对象需要做什么。BDD使用了mockist的途径,但是它对其进行了扩展。我不想在这里深入下去,其与本篇文章的唯一关联就是要说明BDD是试图使用mockist testing进行TDD的一个变种。我把它留给你以根据这个链接来找到更多的信息。
Choosing Between the Differents
在本文中我已经解释了一对不同之处:基于状态的验证和基于行为的验证与classic TDD 和mockist TDD。当在它们之间进行选择的时候,我需要参考哪些信息呢?我将首先开始状态验证与行为验证之间的选择:
第一个要考虑的事情就是上下文(Context),我们正在考虑的是一个简单的协作者(collaborartion)就像order和warehouse,还是一个比较麻烦的就像order和mailService。
如果是一个简单的协作者(collaborator)那么选择就比较容易了。如果我是一个classic TDDer,那么我就不会使用mock,stub或者其他任何类型的double。我使用一个真实的对象并且使用基于状态的验证。如果我是一个mockist TDDer,那么我会选择使用mock和基于行为的验证。根本不需要抉择。
如果是一个麻烦的协作者(collaborator),如果我是一个mockist那就没有选择了,我只会使用mocks和基于行为的验证。如果我是一个classicist,那么我确实有选择,但是那个对于每个人来说都不是重大的抉择。一般来说classicist会选择以一个case一个case为基础,选择对于每种情况来说最容易的路线。
所以就像我们所看到的,基于状态验证和基于行为的验证并不是一个大的抉择。真正的议题是在classic TDD与mockist TDD之间。因为基于状态验证的特性和基于行为验证的特性确实会影响这个议题,而且那也是我最多聚焦的地方。
当时在开始之前,让我先抛出一个极端(边缘)的情况。偶尔你正在进行的事情确实难以通过状态来验证,即使他们不是一个麻烦的协作者(collaborator)。一个突出的例子就是缓存(cache)。最主要的一点就是,在cache中你不能通过缓存的状态来判断其是否命中。这就是一个即使对于一个最坚持的classic TDDer来说也应该广泛使用基于行为的验证的例子。我相信在每一个方向上面的选择都会有一些意外情况发生。
当我们要在classic TDD 与mockist TDD之间做出抉择的时候,有许多的因素需要考虑。所以我粗略的将它们分成了几组:
Driving TDD
Mock Object脱胎于xp社区,而且xp其中的一个实践特性就是其着重于测试驱动开发--一个系统的设计是不断的通过写测试来迭代式的进化的。
因此对于mockist特别的讨论mockist testing对于设计的影响就不奇怪了。特别是他们提倡了一种风格称作为需求驱动开发(need-driven development)。如果使用这种风格,在开始开发一个story的时候,你会先测试你的系统的外部。用一些接口去对象化你的SUT和它的邻居。特别是设计SUT的外围接口。
一旦你的第一个测试运行起来以后,测试中的期望就会为下一步的动作提供一个规格知道,并且同时是下一个测试的起点。你将每一个期望都变成对于协作者的测试。并且重复这样的工作使得系统在同一时间里面只有一个SUT。这种风格也被称为由外而内(outside-in),是一个非常有描述性的名字。它能够在分层系统下面工作得很好。你第一次开始于开发UI层,并且使用对于下面的层使用了mock。然后写下面一层的测试,逐步工作通过一次只处理系统的一个层。这是一个非常结构化的和可控的途径,并且许多人相信它能够帮助指导OO和TDD的初学者。
classic TDD并没有提供同样的指导。你一个进行一个类似的一步步进行的方法,使用stub方法来代替mock方法。要做这件事情,不管什么时候你需要从协作者(collaborator)中得到什么,你只要进行硬代码正好能够相应测试所需要的,能够让SUT工作。当你的测试通过了以后,你就需要将硬代码换成合适的代码。
但是classic TDD还能做其他的事情。一个比较常见的风格就是由中间往外部扩展(middle-out)。在这种风格里,当你需要一个特性的时候,你会决定如果要让这个特性工作,你的领域中需要什么。你取得domain来执行你需要的,当他们能够运行了以后,你再去开发顶层的UI层。做这件事情你不需要去伪造任何事物。许多人喜欢这样的风格,因为它能够让注意点一开始就聚焦于领域模型,能够帮助领域逻辑不会泄露到UI层中去。
我需要强调的是,无论mockist还是classist都只会在一段时间内开发一个story。有一个思想流派是一层一层的构建系统,只有当一层完成以后,才会进行下一层的开发。mockist和classicist都有敏捷的背景,而且跟意愿于细粒度的迭代。结果就是他们一个特性一个特性的工作,而不是一层一层的工作。
Fixture Setup(me:没有想到合适的词去翻译)
如果是使用classic TDD,那么你不光要创建SUT,还需要创建SUT所需要的给测试一个相应的协作者(collaborator)。竟然这个例子中只有几个对象,但是在真正的测试中,一般会涉及一个数量庞大的次要对象(协作者Collaborator)。一般来说这些对象在每次测试运行的时候都会被状态与卸载。
然而对于mockist test来说,只需要创建SUT和其最紧密的邻居的mock。它可以在复杂的功能中避免一些工作。(至少在内存中。)
待续,有点头疼。。。
相关推荐
- **Stub 和 Mock 对象**:Stubs 可以用来替代依赖的对象,提供预设的返回值,而 Mocks 用于验证方法调用和参数。 - **Partial Mocks**:对于那些无法完全模拟的类,Rhino Mocks 提供了部分模拟,允许对部分方法...
Rhino.Mocks是一个广泛使用的.NET单元测试框架,其版本3.6-Build-21提供了高效且用户友好的模拟(mocking)功能。这个框架主要适用于开发者进行单元测试时,需要模拟复杂的对象交互以隔离被测试代码。在本文中,我们...
"Delphi-Mocks-master" 这个文件名暗示了这个压缩包可能包含了一个完整的Mock框架或库,用于在Delphi项目中进行单元测试。通常,这样的库会提供类和方法来创建Mock对象,以及一套API来配置和验证这些对象。开发者...
Google Mock的基础是模拟(Mocks)和期望(Expectations)。模拟对象是原对象的替代品,它们在测试期间代替实际对象工作,可以设置预期行为并验证调用。期望则定义了模拟对象应如何响应特定的方法调用,包括调用的...
在这个过程中,MockBean和SpyBean是两种常用的工具,它们可以帮助我们创建模拟对象(Mocks)和部分模拟对象(Spies),以便在测试中控制依赖关系。下面将详细介绍MockBean和SpyBean的使用方法及其在单元测试中的作用...
angular-mocks.js 1.5.0
在C#中,Rhino Mocks支持两种主要的模拟模式:动态mocks和Stub。动态mocks会验证调用,而Stubs则不会。通常,我们使用动态mocks来验证方法调用,使用Stubs来提供返回值。 以下是一个简单的例子,假设我们有一个`...
### Mocks, Mockito, and Spock 在软件开发过程中,单元测试是确保代码质量的关键环节之一。为了提高单元测试的有效性和可维护性,开发者经常使用模拟对象(Mocks)来代替真实对象进行测试。本文将详细介绍Mocks的...
Rhino Mocks是一款在.NET平台上广泛使用的开源单元测试框架,主要功能是帮助开发者进行模拟对象(Mock Objects)的创建和管理,以便于进行隔离式测试。这个框架以其强大的功能和易用性,使得复杂的测试场景变得更为...
这个名为"angular-mocks.zip"的压缩包文件包含了与AngularJS测试相关的资源,特别是`bower-angular-mocks-master`目录,它很可能是通过Bower包管理器下载的AngularJS Mocks库的源代码。 AngularJS Mocks是一个专门...
GoogleMock 是一个强大的单元测试框架,它扩展了GoogleTest,专为C++程序员设计,用于创建和执行模拟对象,以方便进行单元测试。在本文中,我们将深入探讨GoogleMock的基本概念、用途、安装过程以及如何在项目中使用...
NUnit.Mocks.2.6.2 nunit.mocks.dll 可以直接添加到.NET项目里
- **属性行为**:通过 `Stub<T>()` 创建一个仅能模拟属性访问的模拟对象。 - **部分实现**:对于类,可以使用 `PartialMock<T>()` 或 `PartialMultiMock<T>(params Type[])` 来创建只模拟部分方法或接口的模拟对象。...
model(Person, name: "Fred")安装将此行添加到您的应用程序的gemfile中: gem 'rspec-activemodel-mocks'然后执行: $ bundle如果您使用的是并且已按照那里的安装说明进行操作,那么您都可以设置为stub_model和mock_...
Google Mock 是一个强大的 C++ 单元测试框架,它允许开发者创建模拟对象(Mock Objects)以测试复杂的系统中各个组件的交互。Mock 对象能够模拟真实对象的行为,并且可以预设它们在特定调用下的响应,这对于隔离测试...
`pytest-mock-2.0.0` 是一个专门针对 `pytest` 测试框架的 `mock` 库的轻量级包装器,旨在简化在 `py.test` 版本2中使用 `mock` 进行单元测试的过程。`mock` 库是 Python 中用于模拟对象的工具,它允许我们在测试中...
vue-test-utils 提供了一种 mock 掉 Vue.prototype 的简单方式,不但对测试用例适用,也可以为所有测试设置默认的 mock。 mocks 加载选项 mocks 加载选项 是一种将任何属性附加到 Vue.prototype 上的方式。...
- **Property Mocks**:`mock.PropertyMock()`可以模拟属性的getter和setter方法,用于测试对象属性的访问和修改。 - **Stubbing**:通过Mock,你可以替换掉整个对象或方法链,比如`mock.patch.object(obj, '...
1. **模拟对象(Mocks)**:Google Mock允许你创建模拟对象来代替真实对象。这些模拟对象的行为可以完全自定义,方便在测试中控制它们的行为和期望。 2. **期待(Expectations)**:通过设置期待,你可以指定模拟...