【问题标题】:Do I have to fake a value object in my unit test我是否必须在我的单元测试中伪造一个值对象
【发布时间】:2013-05-02 11:48:46
【问题描述】:

我使用 TDD 编写了一个类,其中包含一个方法(被测方法),该方法将一个简单的值对象作为参数 (range)。

代码:

被测方法如下所示:

public List<string> In(IRange range)
{
     var result = new List<string>();
     for (int i = range.From; i <= range.To; i++)
     {
          //...
     }
     return result;
}

此外,我有一个单元测试来验证我的测试方法:

[TestMethod]
public void In_SimpleNumbers_ReturnsNumbersAsList()
{
    var range = CreateRange(1, 2);
    var expected = new List<string>() { "1", "2" };
    var result = fizzbuzz.In(range);
    CollectionAssert.AreEqual(expected, result);
}

private IRange CreateRange(int from, int to)
{
    return new Fakes.StubIRange() 
    { 
        FromGet = () => { return from; }, 
        ToGet = () => { return to; } 
    };
}

问题:

我读过 Roy Osherove 关于单元测试的书(“单元测试的艺术”)。他在那里说

"外部依赖项(文件系统、时间、内存等)应该是 被存根替换”

external 依赖是什么意思?我的值对象(范围)是否也是应该伪造的外部依赖项?我应该伪造一个类的所有依赖项吗?

谁能给个建议

【问题讨论】:

  • 这意味着您应该测试正在审查的进程,而不是文件系统/时间/内存。不要依赖依赖项的具体实现,因为它们可以改变。您在 Stubbing IRange 中是正确的,因为您正在尝试使用任何 IRange 而不仅仅是一个特定的实现来测试方法 In
  • IRange 的实现有多少?这是您引入的纯粹用于支持模拟的接口吗?
  • @Romoku 如果 IRange 的实现遵循 LSP,那么使用哪个实现来提供测试并不重要,并且使用一个具体的实现不会使测试无效。
  • 你不能用IEnumerable&lt;int&gt;替换那个IRange接口吗?您的In 方法可以定义为In(IEnumerable&lt;int&gt; range),您可以使用fizzbuzz.In(Enumerable.Range(1, 2)) 调用它。
  • 许多函数式语言允许您将这种记录类型视为可互换的元组。例如在 Nemerle 你可以说def (min, max) = SomeRange(0, 100); 然后方法签名将是fizzbuzz.In(range : int * int) 允许你传入任何范围,或者只是一个元组

标签: c# unit-testing dependency-injection tdd


【解决方案1】:

TL;DR

做最简单的事情来解决你的问题。


我使用 TDD 的时间越长,我就越体会到务实的价值。编写超级孤立的单元测试本身并不是一个价值。这些测试可以帮助您编写易于理解并解决正确问题的高质量代码。

如果您需要能够切换到另一个具体的范围实现而无需修改依赖于它的代码,那么为范围类添加一个接口是一个好主意。

但是,如果您没有这种需要,添加接口并没有真正的目的,但它确实增加了一些复杂性,这实际上使您远离编写易于理解的代码来解决问题的目标。

注意不要过多考虑未来可能发生的变化。 YAGNI 是一个很好的原则。如果您一直在做 TDD,那么如果将来有实际需要,重构代码不会有任何问题,因为您有可靠的测试可以依赖。

一般而言,我不会将适当的值对象视为依赖项。如果它足够复杂以至于您在测试时让其他代码使用它感到不舒服,那听起来它实际上更像是一种服务。

【讨论】:

    【解决方案2】:

    单元测试应该独立运行(完全在内存中),而不必触及任何外部系统,例如文件系统、数据库、Web 服务、邮件服务、系统时钟或任何缓慢、难以设置的系统,或不确定的(例如不断变化的系统时间)。

    为了能够做到这一点,您应该抽象掉那些允许您在测试中模拟它们的外部依赖项。

    然而,单元测试更进一步。在单元测试中,您通常只想测试单个方法或单个类的逻辑。您对验证多个组件如何集成不感兴趣,而只想验证单个类的逻辑是否正确,以及它与其他组件的通信是否正确。

    为了能够做到这一点,您需要伪造那些其他组件(类的依赖项)。所以一般来说,你确实应该伪造一个类的所有依赖项(包含行为)。

    【讨论】:

    • +1 原则上同意,但我认为价值对象应该区别对待。值对象(在 DDD 术语中)不需要被伪造/模拟,因为它只是一个数据容器 - 似乎这里可能就是这种情况。
    • @MattDavey:我同意。这就是为什么我在回答中说“包含行为”和“一般”。
    • @MUG4N:既然那个 Range 类只包含数据而没有逻辑,那么你伪造它或在其上添加抽象的原因是什么?
    • 您希望 Range 类在接口实际有帮助的地方发生什么变化?当您更改 Range 类时,您可能还必须更改 IRange 接口。而且您的单元测试目前不是很健壮。我什至会说它在这一点上非常难以维护。就像现在一样,IRange 接口是一个无用的抽象。它对你没有帮助。由于 Range 不包含任何逻辑,因此永远不需要抽象。
    • 将值对象视为离散值很重要。你会为intsdoubles 创建一个接口吗?同样的原则也适用于值对象。
    猜你喜欢
    • 2013-12-23
    • 2012-07-02
    • 2012-11-01
    • 1970-01-01
    • 2011-02-10
    • 2018-04-30
    • 1970-01-01
    • 2016-09-08
    • 1970-01-01
    相关资源
    最近更新 更多