4

我正在尝试通过将 TDD 应用于我的一个简单项目来学习它。一些细节(和更早的问题)在这里:

TDD:帮助编写可测试类

具体是我有一个 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 为假或真以通过测试的逻辑,而实际上该测试不应该关心。问题是不同的(imo),因为类本身不是问题。

本质上,我希望能够声明一个 IsValid 为 false 或 true 的 PurchaseOrder,而不需要更多地了解 PurchaseOrder 是什么。

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

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

谢谢!

4

7 回答 7

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 类。我的经验法则是什么时候先使用虚拟。只是为了创建一个接口,这样我就可以在没有任何架构原因的情况下模拟一个对象,这对我来说是一种代码味道。

于 2009-01-26T15:50:53.743 回答
2

...这个测试太耦合了,我必须知道什么使 PurchaseOrder IsValid 为假或真才能通过测试的逻辑,而实际上这个测试不应该关心...

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

一般来说,我尽量避免为诸如 PurchaseOrder 之类的“实体”对象编写接口,除非除了测试之外还有其他理由这样做(例如,生产中有多种 PurchaseOrders,而接口是建模的最佳方式) )。

当测试表明您的生产代码可以设计得更好时,这很棒。但是,仅仅为了使测试成为可能而更改您的生产代码并不是那么好。

好像我写的不够多,这里有另一个建议——这就是我在现实生活中实际解决这个问题的方式。

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

于 2009-01-26T15:49:21.843 回答
1

我最近问了一个关于测试的类似问题。不要忘记这一点:做你需要做的最简单的事情,然后在必要时重构。我个人尽量把大局放在心上,但我也抵制过度设计我的解决方案的冲动。您可以在测试类中添加两个 PurchaseOrder 字段,其中一个有效,一个无效。使用这些字段将您的 PurchaseOrderCollection 置于您想要测试的状态。您最终需要学习如何模拟,但在这种情况下,您不需要大锤,因为普通锤子可以解决问题。通过使用模拟 PurchaseOrder 而不是处于所需状态的具体 PurchaseOrder 不会获得任何价值。

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

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

于 2009-01-26T16:37:19.547 回答
1

我可能在这里遗漏了一些上下文,但在我看来,您必须以示例的方式“耦合”您的测试,否则您并没有真正测试任何东西(除了 IsValid 属性,这是微不足道的)。

模拟采购订单一无所获——你测试的是模拟,而不是真正的类

使用存根 - 同样的事情

使用 TDD 时可以进行白盒测试(如果不是强制性的)

于 2009-01-26T17:11:54.297 回答
1

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

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

于 2009-09-06T20:01:20.013 回答
0

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

于 2009-01-26T15:36:27.377 回答
0

这两个都是有效的想法吗?

是的。

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

于 2009-01-26T16:29:02.120 回答