TDD:编写可测试类的帮助

7
我有一个简单的应用程序,想尝试使用TDD进行开发。我以前从未使用过TDD,直到我发现了ASP.NET-MVC才知道它是什么。(我的第一个MVC应用程序有单元测试,但它们很脆弱、紧密耦合、需要太多维护,并被放弃了——我已经学会了单元测试!= TDD)。
应用程序背景:
我有一个采购订单的文本转储,读入为字符串。我需要解析文本并返回新的采购订单号、新的行项目编号、旧的采购订单号和旧的采购订单行号。非常简单。
现在我只在处理新的采购订单详细信息(编号/行),并且有一个像这样的模型:
public class PurchaseOrder
{
    public string NewNumber {get; private set;}
    public string NewLine {get; private set;}

    new public PurchaseOrder(string purchaseOrderText)
    {
        NewNumber = GetNewNumber(purchaseOrderText);
        NewLine = GetNewLine(purchaseOrderText);
    }

    // ... definition of GetNewNumber / GetNewLine ...
    //  both return null if they can't parse the text
}

现在我想添加一个名为“IsValid”的方法,只有当“NewNumber”和“NewLine”都不为空时才应该返回true。因此,我想这样测试它:

public void Purchase_Order_Is_Valid_When_New_Purchase_Order_Number_And_Line_Number_Are_Not_Null()
{
    PurchaseOrder order = new PurchaseOrder()
    {
        NewNumber = "123456",
        NewLine = "001"
    };

    Assert.IsTrue(order.IsValid);
}

这很容易,但允许公共setter和无参数构造函数似乎是一个糟糕的妥协。因此,另一种选择是在构造函数中输入'purchaseOrderText'值,但这样我也要测试'GetNewNumber'和'GetNewLine'的代码。
我有点困惑如何编写可测试的类,同时尝试保持其符合模型所需的限制。这似乎是一个常见的问题,所以我认为我可能是忽略了一个明显的概念。
5个回答

8

一种解决方法是不让构造函数去执行工作:

public class PurchaseOrder
{
    public PurchaseOrder(string newNumber, string newLine)
    {
        NewNumber = newNumber;
        NewLine = newLine;
    }
    // ...
}

那么测试就变得简单且隔离——您不会同时测试GetNewNumberGetNewLine

为了帮助使用PurchaseOrder,您可以创建一个工厂方法来组合它:

public static PurchaseOrder CreatePurchaseOrder(string purchaseOrderText)
{
   return new PurchaseOrder(
     GetNewNumber(purchaseOrderText),
     GetNewLine(purchaseOrderText));
}

这也更好地使用了单一职责原则——从文本到数字和行项目的转换本质上是一个 I/O 函数,而不是购买订单固有的功能。如果您的文本转储格式发生变化,您的购买订单不应该需要改变。 - Jacob Mattison
我想补充一点,这正是TDD擅长揭示的设计实现类型——如果您无法对某些内容进行合理的测试,那么它可能做了太多的事情。 - Jacob Mattison
感谢您的回答和评论。我同意两者,并且必须承认我有点兴奋看到TDD这样做(像广告一样有效)。这个程序非常简单,可以只在主函数中作为一个过程性函数完成,但我想你必须通过实践学习,而不仅仅是阅读它。 - anonymous

2

不要将设置器(public setters)设为公共的,而是将它们设为 internal,并在主项目中使您的测试程序集(InternalsVisibleTo)可见。这样,您的测试可以看到您的内部成员,但其他人则不能。

在您的主项目中,添加如下内容;

[assembly: InternalsVisibleTo( "UnitTests" )]

其中 UnitTests 是您的测试程序集的名称。


1
在测试项目中为您的类创建一个私有访问器,然后使用该访问器设置测试的属性。在VS中,您可以通过右键单击类、选择“创建私有访问器”并选择测试项目来创建私有访问器。在您的测试中,您可以像这样使用它:
public void NameOfTest()
{
    PurchaseOrder_Accessor order = new PurchaseOrder_Accessor();
    order.NewNumber = "123456";
    order.NewLine = "001";
    Assert.IsTrue(order.IsValid);
}

如果您有一个默认构造函数,您可以这样做:
public void NameOfTest()
{
    PurchaseOrder order = new PurchaseOrder()
    PurchaseOrder_Accessor accessor =
                new PurchaseOrder_Accessor( new PrivateObject(order) );
    accessor.NewNumber = "123456";
    accessor.NewLine = "001";
    Assert.IsTrue(order.IsValid);
}

哇,我甚至不知道有访问器这个东西。找到了MSDN上的文章,已经加入我的待办事项列表中 :) 谢谢! - anonymous

0
我会创建一个新的构造函数 public PurchaseOrder(string newNumber, string newLine)。在我看来,你很可能需要它。

0

有点跑题 :)

使用TDD,首先编写单元测试,然后只需编写足够的代码来通过测试,之后进行重构。通过先编写测试,您应该确切地知道如何编程以使您的代码可测试!

仅供参考...


我正在尝试这个。我的下一个测试是IsValid,这是一个很容易编写的测试。在我编写测试之后,很明显代码不能支持测试,所以我需要重构或重写测试(但我不知道该选哪个)。谢谢! - anonymous

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