TDD:Stub,Mock或以上都不是

时间:2009-01-26 15:29:03

标签: c# tdd mocking stub

我正在尝试将TDD应用于我的一个简单项目。一些细节(以及之前的问题)在这里:

TDD: Help with writing Testable Class

具体是我有一个PurchaseOrderCollection类,它有一个私有的PurchaseOrders列表(在构造函数中传入),而PurchaseOrders有一个布尔属性IsValid。 PurchaseOrderCollection具有属性HasErrors,如果列表中的任何PurchaseOrders将IsValid设置为false,则返回true。这是我想要测试的逻辑。

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{
    List<PurchaseOrder> orders = new List<PurchaseOrder>();

    orders.Add(new PurchaseOrder(--some values to generate IsValid false--));
    orders.Add(new PurchaseOrder(--some values to generate IsValid true--));

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

这与我之前的问题类似,因为这个测试太过耦合了,我必须知道什么使得PurchaseOrder IsValid为false或者true才能通过测试,而这个测试真的不应该关心。问题是不同的(imo)因为类本身不是问题。

基本上我希望能够在不知道任何有关PurchaseOrder的内容的情况下声明具有IsValid false或true的PurchaseOrder。

根据我有限的TDD知识,这是你使用Stubs或Mocks的东西。我的主要问题是,这是正确的吗?或者我应该使用不同的方法吗?或者我是完全有缺陷的,我只是在编写这个测试并且认为它错了?

我最初的想法是使用某种模拟框架并创建一个始终返回true或false的PurchaseOrder。从我读过的内容来看,我需要声明IsValid是虚拟的。所以我的第二个想法是改变它以添加IPurchaseOrder作为PurchaseOrder的接口,并创建一个始终返回false或true的假PurchaseOrder。这两个都是有效的想法吗?

谢谢!

7 个答案:

答案 0 :(得分:6)

您可以创建存根或模拟,从而走在正确的轨道上。我更喜欢使用Mocking框架。

使用模拟框架的工作原理是你想要模拟你的PurchaseOrder类,从而抽象出它的实现。然后设置调用IsValid的期望值以及调用它时返回此值。

使用Moq的示例,如果您使用的是C#3.0和.NET Framework 3.5:

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{    
    var mockFirstPurchaseOrder = new Mock<IPurchaseOrder>();
    var mockSecondPurchaseOrder = new Mock<IPurchaseOrder>();

    mockFirstPurchaseOrder.Expect(p => p.IsValid).Returns(false).AtMostOnce();
    mockSecondPurchaseOrder.Expect(p => p.IsValid).Returns(true).AtMostOnce();

    List<IPurchaseOrder> purchaseOrders = new List<IPurchaseOrder>();
    purchaseOrders.Add(mockFirstPurchaseOrder.Object);
    purchaseOrders.Add(mockSecondPurchaseOrder.Object);

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

编辑:
在这里,我使用了一个界面来创建PurchaseOrder的模拟,但是你没有。您可以将IsValid标记为虚拟并模拟PurchaseOrder类。我的经验法则是哪种方式首先使用虚拟。只是为了创建一个界面,所以我可以在没有任何架构原因的情况下模拟一个对象,这对我来说就是代码味道。

答案 1 :(得分:2)

  

......这个测试太过于偶然   必须知道是什么造成的逻辑   PurchaseOrder IsValid为false或true   通过测试,真正进行此测试   不应该在乎...

我实际上反过来说 - 为了让你的测试知道有效性被建模为采购订单中的布尔值意味着你的测试对PurchaseOrder的实现了解太多(假设它实际上是对PurchaseOrderCollection的测试) )。使用真实世界的知识(即有效或无效的实际值)创建适当的测试对象没有问题。最终,这真的是你正在测试的(如果我给我的收藏品一个带有荒谬值的购买订单,它会正确告诉我有错误)。

一般情况下,我尽量避免为“实体”对象(如PurchaseOrder)编写接口,除非有一些理由除了测试之外这样做(例如,生产中有多种PurchaseOrders,接口是最佳方式模仿那个)。

当测试显示您的生产代码可以更好地设计时,它非常棒。但是,更改生产代码只是以使测试成为可能并不是那么好。

好像我写得不够,这是另一个建议 - 这就是我在现实生活中实际解决这个问题的方法。

创建具有接口的PurchaseOrderValidityChecker。在设置isValid布尔值时使用它。现在创建一个有效性检查器的测试版本,它允许您指定要给出的答案。 (请注意,此解决方案可能还需要PurchaseOrderFactory或用于创建PurchaseOrders的等效项,以便在创建PurchaseOrderValidityChecker时为每个采购订单提供引用。)

答案 2 :(得分:1)

我最近问了一些similar question关于测试的问题。不要忘记这一点:做你需要做的最简单的事情,然后在必要时进行重构。我个人试图牢记大局,但我也抵制过度强调我的解决方案的冲动。您可以将两个PurchaseOrder字段添加到测试类中,其中一个是有效的,一个是无效的。使用这些字段将PurchaseOrderCollection置于要测试的状态。你需要学习如何最终模拟,但在这种情况下,当常规锤子解决问题时你不需要大锤。使用模拟PurchaseOrder而不是处于所需状态的具体PurchaseOrder,您无法获得任何价值。

最重要的是,您通过测试PurchaseOrderCollection的行为而不仅仅是测试PurchaseOrderCollection的状态获得了更多。在您的测试验证PurchaseOrderCollection可以进入其不同状态后,更重要的测试是行为测试。通过您认为合适的方式(在所需状态下模拟或新建具体类)将您的采购订单集合置于有效和无效状态,并测试PurchaseOrderCollection的每个状态的逻辑是否正确执行而不仅仅是PurchaseOrderCollection只是处于有效/无效状态。

PurchaseOrderCollection将始终依赖于另一个类,因为它是一个专门的集合。知道IPurchaseOrder具有IsValid属性与知道具体的PurchaseOrder具有IsValid属性没有任何不同。我坚持使用最简单的方法,例如:一个具体的PurchaseOrder,除非你有充分的理由相信你的系统中会有多种类型的PurchaseOrders。那时,PurchaseOrder界面会更有意义。

答案 3 :(得分:1)

我可能在这里错过了一些上下文,但在我看来,你必须以你的例子的方式“结合”你的测试,否则你并没有真正测试任何东西< / em>(除了IsValid属性,这是微不足道的。)

嘲笑采购订单没有任何收获 - 你已经测试了模拟,而不是真正的类

使用存根 - 同样的事情

使用TDD

时可以使用白盒测试,如果不是必须的话

答案 4 :(得分:1)

首先,请记住您正在测试集合,而不是PurchaseOrder,因此这是您的努力所在。这取决于PurchaseOrder的复杂程度。如果它是一个具有明显行为的简单实体,那么创建实例可能就有意义了。如果它更复杂,那么按照你的描述提取界面是有意义的。

提出的下一个问题是该界面中的内容。集合中的对象需要执行什么角色?也许你只需要知道它们是否有效,在这种情况下你可以提取IValidatable并缩小代码中的依赖关系。我不知道在这种情况下是什么,但我经常发现我可以使用接口将我推向更集中的代码。

答案 5 :(得分:0)

我不是单元测试的专家,但这是我过去所做的。如果你有一个可以有效/无效的PurchaseOder类,那么我确定你也有那些单元测试,看看它们是否确实有效。为什么不调用这些方法来生成有效和无效的PurchaseOrder对象,然后将其添加到您的集合中?

答案 6 :(得分:0)

  

这些都是有效的想法吗?

您还可以创建一个Object Mother,它可以返回有效和无效的PurchaseOrders。