TDD:存根、模拟还是无?

5
我正在尝试通过将其应用于我的一个简单项目来学习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的逻辑才能通过测试,而实际上这个测试不应该关心这些。问题不同(在我看来),因为类本身不是问题。
本质上,我想能够声明一个PurchaseOrder,其IsValid为false或true,而不需要了解有关PurchaseOrder的任何其他信息。
根据我有限的TDD知识,这是使用存根或模拟对象的方法。我的主要问题是,这样做正确吗?还是我应该使用其他方法?或者我完全错了,只是在错误地编写和思考这个测试?
我的最初想法是只需使用某种模拟框架并创建一个始终返回true或false的PurchaseOrder。然而,根据我所读的,我需要将IsValid声明为虚拟的。因此,我的第二个想法是将其更改为为PurchaseOrder添加IPurchaseOrder作为接口,并创建一个始终返回false或true的虚假PurchaseOrder。这两个想法都是有效的吗?
谢谢!
7个回答

6
你选用创建存根或模拟对象的方式是正确的,我更倾向于使用模拟框架。使用模拟框架的方法是,你需要对 PurchaseOrder 类进行模拟,从而抽象出它的实现。然后设置期望,即当调用 IsValid 方法时返回该值。如果你正在使用 C# 3.0 和 .NET Framework 3.5,可以使用 Moq 进行示例。
[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类。我的经验法则是首先使用虚拟方法。只是为了创建一个接口,以便无需任何架构原因就可以模拟对象,对我来说是一种代码异味。


2
......这个测试太耦合了,因为我必须知道什么使得PurchaseOrder IsValid为false或true才能通过测试,但实际上这个测试不应该关心这些......

我认为反过来更好——你的测试知道在购买订单中有效性被建模为布尔值,这意味着你的测试对PurchaseOrder的实现了解过多(考虑到它实际上是PurchaseOrderCollection的测试)。我不反对使用实际的知识(即实际的有效或无效值)来创建适当的测试对象。最终,这确实是你要测试的内容(如果我给我的集合一个具有荒谬值的购买订单,它是否会正确地告诉我有错误)。

一般情况下,我尽量避免为“实体”对象(如采购订单)编写接口,除非有除了测试之外的其他原因(例如,在生产中有多种类型的采购订单,接口是最好的建模方式)。

当测试揭示出你的生产代码可能设计得更好时,这很棒。然而,仅仅为了让测试变得可能而改变生产代码就不是那么好了。

如果我还没有写够,这里有另一个建议——这也是我在现实生活中实际解决这个问题的方式。

创建一个PurchaseOrderValidityChecker,它有一个接口。在设置isValid布尔值时使用它。现在创建一个测试版本的有效性检查器,让你可以指定要给出哪个答案。(请注意,这个解决方案可能还需要一个PurchaseOrderFactory或相当的东西来创建PurchaseOrders,以便每个采购订单在创建时都可以得到对PurchaseOrderValidityChecker的引用。)


我也喜欢你的答案,但只能选一个~在这种情况下,构成有效采购订单的逻辑已经超出了单个采购订单可以计算的范围。虽然我还没有编写代码,但将逻辑提取到PurchaseOrderValidityChecker似乎是可行的。 - anonymous
我最初的犹豫是在尝试在大型不可测试类和将每个方法都提取到自己的类中进行平衡。我想经验会对此有所帮助。感谢您的回答。 - anonymous

1

我最近就测试问题询问了一个有些类似的问题。别忘了这一点:做需要做的最简单的事情,然后在必要时进行重构。我个人试着保持大局观,但我也抵制过度工程化解决方案的冲动。你可以在测试类中添加两个PurchaseOrder字段,其中一个是有效的,另一个是无效的。使用这些字段将PurchaseOrderCollection放入您想要测试的状态。最终你需要学会如何模拟,但在这种情况下,当普通锤子能解决问题时,你不需要使用大锤。通过使用一个模拟的PurchaseOrder而不是处于您所需状态的具体PurchaseOrder,您不会获得任何价值。

最重要的是,测试PurchaseOrderCollection的行为比仅测试其状态更有价值。当您的测试验证了PurchaseOrderCollection可以处于其不同的状态时,那么更重要的测试就是行为测试。通过适当的方式(模拟或新建具有所需状态的具体类)将您的采购订单集合放入有效和无效状态,并测试PurchaseOrderCollection每个状态的逻辑是否正确执行,而不仅仅是PurchaseOrderCollection是否仅处于有效/无效状态。
由于PurchaseOrderCollection是一个专业化的集合,它总是依赖于另一个类。知道IPurchaseOrder具有IsValid属性并不比知道具体的PurchaseOrder具有IsValid属性更有用。除非您有很多理由相信系统中会有多种类型的PurchaseOrders,否则我会坚持最简单的方法,例如使用具体的PurchaseOrder。在那时,PurchaseOrder接口将更有意义。

1

我可能缺少一些上下文,但在我看来,你必须像你的例子那样“耦合”你的测试,否则你实际上并没有测试任何东西(除了一个微不足道的IsValid属性)。

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

使用存根 - 同样的事情

当使用TDD时,白盒测试是可以的 - 如果不是强制性的


对不起,可能的背景是我有涵盖IsValid和PurchaseOrder的测试。如果我更改IsValid的定义或修改PurchaseOrder之类的内容,我希望IsValid测试会随之修改。但我只是不想让HasErrors被更改——它只关心单个属性。 - anonymous
@huey:我仍然没有看到问题-根据机制的描述和测试代码的意图,您的测试看起来是正确的。您是否预期可能不会发生的事情?YAGNI!;-) - Steven A. Lowe

1
首先,请记住您正在测试集合,而不是“PurchaseOrder”,因此需要努力的地方在这里。这取决于“PurchaseOrder”是否复杂。如果它是一个具有明显行为的简单实体,那么只需创建实例可能就足够了。如果它更加复杂,则有必要像您所描述的那样提取一个接口。
接下来引发的问题是该接口中包含什么内容。集合中的对象需要执行什么角色?也许你只需要知道它们是否有效,在这种情况下,您可以提取“IValidatable”,并缩小代码中的依赖关系。我不知道在这种情况下是什么真相,但我经常发现可以使用接口将我推向更专注的代码。

0

我并不是单元测试的专家,但这是我过去所做的。如果你有一个可以有效/无效的采购订单类,那么我相信你也会为此编写单元测试,以检查它们是否确实有效。为什么不调用这些方法来生成有效和无效的采购订单对象,然后将其添加到你的集合中呢?


0
这两个想法都是有效的吗?
是的。
您还可以创建一个对象生成器,它可以返回有效和无效的采购订单。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接