【问题标题】:How can I avoid adding getters to facilitate unit testing?如何避免添加 getter 以促进单元测试?
【发布时间】:2011-07-19 19:52:39
【问题描述】:

在阅读了 blog post 提到为了方便测试而公开一个公共 getter 似乎是错误的之后,我找不到任何更好实践的具体示例。

假设我们有这个简单的例子:

public class ProductNameList
{
    private IList<string> _products;
    public void AddProductName(string productName)
    {
        _products.Add(productName);
    }
}

假设出于面向对象设计的原因,我没有必要公开公开产品列表。那么我如何测试 AddProductName() 是否完成了它的工作? (也许它添加了两次产品,或者根本没有。)

一种解决方案是公开 _products 的只读版本,我可以在其中测试它是否只有一个产品名称——我传递给 AddProductName() 的那个。

我之前提到的博客文章说它更多的是关于交互(即是否添加了产品名称)而不是状态。但是,state 正是我正在检查的内容。我已经知道 AddProductName() 已被调用——我想在该方法完成工作后测试对象的状态是否正确。

免责声明:虽然这个问题类似于 Balancing Design Principles: Unit Testing,(1) 语言不同(C# 而不是 Java),(2) 这个问题有示例代码,以及 (3) 我觉得这个问题没有得到充分的回答(即,代码将有助于演示概念)。

【问题讨论】:

  • 我认为让这件事变得更棘手的是AddProductName()行为是它改变了ProductNameList的状态。跨度>

标签: c# unit-testing


【解决方案1】:

单元测试应该测试公共 API。

如果你“无需公开产品清单”,那你又何必关心AddProductName 是否完成了它的工作呢?如果该列表完全是私有的并且永远不会影响其他任何东西,它会产生什么影响?

找出影响 AddProductName可以使用 API 检测到的状态有什么影响,并对其进行测试。

这里有非常相似的问题:Domain Model (Exposing Public Properties)

【讨论】:

  • 所以如果我有Add,并且我也有Frob,它作用于我添加的东西,如果我添加bar但它没有被冻结,那就是破碎的? FrobAdd?
  • 我会看一下域模型问题——感谢您提供的链接。 ProductNameList 类中的其他方法/属性可以使用私有列表。类的使用者并不关心名称是如何存储的(队列、堆栈、列表、字典)——它们只需要被存储。 AddProductName() 是消费者和存储名称的实际数据结构之间的一层。
  • Anthony:这不是单元测试会为你决定的事情,因为 Add 和 Frob 是只对彼此有意义的操作。
  • OTOH,如果你有一个 Frob、一个 Bar、一个 Foo,它们都作用于添加的对象,并且代码更改会破坏所有三个测试,那么你可以确定问题出在 Add .
【解决方案2】:

您可以保护您的访问器,以便您可以模拟,或者您可以使用 internal 以便您可以在测试中访问该属性,但正如您所建议的那样,IMO 将是错误的。

我认为有时我们会非常想确保我们代码中的每一个小东西都经过测试。有时我们需要退后一步,问我们为什么要存储这个值,它的目的是什么?然后,我们可以开始测试组件的行为是否正确,而不是测试该值是否已设置。

编辑

你可以在你的场景中做的一件事是有一个混蛋构造函数,你可以在其中注入一个 IList,然后测试你是否添加了一个产品:

public class ProductNameList
{
    private IList<string> _products;

    internal ProductNameList(IList<string> products)
    {
        _products = products;
    }
    ...
}

然后你会像这样测试它:

[Test]
public void FooTest()
{
    var productList = new List<string>();

    var productNameList = new ProductNameList(productList);

    productNameList.AddProductName("Foo");

    Assert.IsTrue(productList[0] == "Foo");
}

您需要记住让内部组件对您的测试程序集可见。

【讨论】:

  • 我同意我们对测试的关注可能超出了我们应有的程度。我的目标是测试是否发生了“添加产品名称”的行为。我将如何验证这种行为?
  • @geoffmazeroff 好的,那么添加产品名称的目的是什么,它是如何使用的?还有更多我们看不到的课程吗?您拥有的另一个选择是使用混蛋构造函数(请参阅我的编辑),这需要一个专门用于测试的构造函数,但因为它是内部的,所以没有任何泄漏。
  • 我并没有真正想过还有什么其他方法,但假设我有一个方法public IList&lt;string&gt; GetProductNames。如果我对此进行单元测试,我有一个循环依赖:我无法使用 Get 测试 Add,也无法测试没有 Add 的 Get。 (我非常喜欢一次只测试一件事。)我认为您的内部构造函数技巧可能是我正在寻找的解决方案!
  • 就是这样 - 没有 Get 就无法测试 Add。没有 Get,Add 没有任何意义。 Get 没有 Add 就没有任何意义(除了返回一个空列表。)测试 Add 以您期望的方式修改类的内部是测试 Add 的错误方法。一个类的内部应该完全独立于它的 API。单元测试的重点是在您更改内部后验证该类是否符合其 API。只要 API 正常运行,如果您更改内部结构,您的测试就不会失败。而且您存储姓名的方式不是 API 的一部分。
  • @geoffmazeroff 正如弗拉迪斯拉夫所说,除非您设计课程以适应错误的测试,否则您不能没有另一个测试一个。您应该尽量不要编写强制类应该如何操作的测试。调用 AddProductName 的人并不关心产品名称是如何存储的,只要它被存储并且可以被检索到。也许您应该使用 StoryQ 之类的东西来查看 BDD 测试。 BDD 是关于行为而不是实现。
【解决方案3】:

使用_products protected 而不是private。在你的模拟中,你可以添加一个访问器。

【讨论】:

    【解决方案4】:

    要测试 AddProductName() 是否有效,而不是为 _ProductNames 使用公共 getter,而是调用 GetProductNames() - 或 API 中定义的等效项。这样的函数不一定在同一个类中。

    现在,如果您的 API 没有公开任何获取产品名称信息的方法,那么 AddProductName() 就没有可观察到的副作用(在这种情况下,它是一个毫无意义的函数)。

    如果 AddProductName() 确实有副作用,但它们是间接的 - 比如说,ProductList 中的一个将产品名称列表写入文件的方法,那么 ProductList 应该分为两个类 - 一个管理列表,另一个另一个调用它的是 Add and Get API,并执行副作用。

    【讨论】:

    • AddProductName() 确实有副作用——它修改了其他方法/属性最终可能使用的私有字段。
    • 那么您的单元测试应该首先调用 AddProductName(),然后调用那些依赖于产品名称列表的其他方法/属性,并验证它们的行为是否一致。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-01-18
    • 1970-01-01
    • 2016-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多