【问题标题】:Unit testing and checking private variable value单元测试和检查私有变量值
【发布时间】:2010-11-08 17:53:30
【问题描述】:

我正在使用 C#、NUnit 和 Rhino Mocks 编写单元测试。 以下是我正在测试的课程的相关部分:

public class ClassToBeTested
{
    private IList<object> insertItems = new List<object>();

    public bool OnSave(object entity, object id)
    {
        var auditable = entity as IAuditable;
        if (auditable != null) insertItems.Add(entity);

        return false;            
    }
}

我想在调用 OnSave 后测试 insertItems 中的值:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to insertItems array            
}

这方面的最佳做法是什么?我考虑过将 insertItems 添加为具有公共 get 的属性,或者将 List 注入 ClassToBeTested,但不确定我是否应该修改代码以进行测试。

我已经阅读了很多关于测试私有方法和重构的帖子,但这是一个如此简单的类,我想知道最好的选择是什么。

【问题讨论】:

  • 为什么 OnSave 总是返回 false? =/在.Add之后是否应该在“if(auditable!= null)”的条件内返回true?当然,它需要用括号括起来。
  • 这个问题已经有将近 9 年的历史了,所以我不太记得了。有可能因为我使用的是 TDD,所以当我发布这个问题时,我还没有实现 OnSave 方法的返回逻辑。

标签: c# unit-testing nunit


【解决方案1】:

快速回答是,您永远不应该从您的单元测试中访问非公共成员。它完全违背了拥有测试套件的目的,因为它将您锁定在您可能不想保持这种方式的内部实现细节中。

更长的答案与接下来要做什么有关?在这种情况下,重要的是要了解实现的原因(这就是 TDD 如此强大的原因,因为我们使用测试来指定预期的行为,但我感觉你没有使用 TDD)。

在您的情况下,首先想到的问题是:“为什么将 IAuditable 对象添加到内部列表中?”或者,换一种说法,“这个实现的预期外部可见结果是什么?”根据这些问题的答案,这就是您需要测试的内容。

如果您将 IAuditable 对象添加到您的内部列表,因为您以后想将它们写入审计日志(只是一个疯狂的猜测),然后调用写入日志的方法并验证是否写入了预期的数据。

如果您将 IAuditable 对象添加到您的内部列表中,因为您想收集针对某种后来约束的证据,那么请尝试对其进行测试。

如果您添加代码的原因不明,请再次删除它:)

重要的是测试行为而不是实现是非常有益的。它也是一种更健壮和可维护的测试形式。

不要害怕修改您的被测系统 (SUT) 以提高可测试性。只要您的添加在您的领域中有意义并遵循面向对象的最佳实践,就没有问题 - you would just be following the Open/Closed Principle

【讨论】:

  • 你的“疯狂猜测”是正确的。我已将测试更改为调用 OnSave 方法,然后调用写入日志数据的方法。然后我断言数据已被相应地记录,而不注意 IList。如果在单个测试中从 SUT 调用两个方法是可以接受的做法,那么这将是我的首选解决方案。谢谢。
  • Arrange-Act-Assert 或四阶段测试中的事件,调用多个 SUT 成员是完全合法的。一个人应该真正努力遵循一次只测试一件事的原则,但我认为你的方法很符合这个原则。本质上,调用 OnSave 方法只是安排/设置夹具阶段的一部分。当您调用日志记录方法时,将执行 Act 阶段。没有违反最佳实践:)
  • 这样的声明(永远不应该测试私有接口)让我感到困惑。让我来探讨一下:例如,一个工厂应该在所有客户端释放资源后将借出的资源归还给空闲池。由于不需要向客户端公开池状态,因此它是私有的。在这种情况下应该如何验证正确的行为? (第 1 部分,共 2 部分)
  • 我同意你上面提出的方法。但是,如果测量空闲池的状态不切实际怎么办?实用主义不会推动一个人测试内部状态,充分理解,与任何行为发生变化的情况一样,如果后来有人实施不同的方案,这些测试将需要重构?事实上,他们会亮红灯并告诉实施者,这是设计使然。我一般不会质疑您的回答是否明智。我真的很感兴趣,你会建议如何解决这种不太常见的情况。 (第 2 部分,共 2 部分)
  • @bRadGibson 我认为更改 SUT 的接口是 TDD 过程中完全正常的一部分:blog.ploeh.dk/2011/11/10/TDDimprovesreusability
【解决方案2】:

您不应该检查添加项目的列表。如果您这样做,您将为列表中的 Add 方法编写单元测试,而不是为 您的 代码编写测试。只需检查 OnSave 的返回值;这就是你想要测试的全部内容。

如果您真的关心 Add,请将其从等式中模拟出来。

编辑:

@TonE:在阅读了您的 cmets 之后,我想说您可能想要更改当前的 OnSave 方法以让您了解失败。如果转换失败等,您可以选择抛出异常。然后您可以编写一个预期和异常的单元测试,而另一个则没有。

【讨论】:

  • 谢谢 - 添加更多上下文, OnSave 是一个必须返回 false 的覆盖,所以我不能在这里使用返回值。我想我想要做的是检查列表的 Add 方法是否被调用,而不是检查列表的内容。这让我回到模拟列表并注入它,或者还有其他方法......
  • 感谢您的建议,但使用未实现 IAuditable 的对象调用 OnSave 是一个预期的条件 - 在这种情况下,我不热衷于抛出异常。在许多情况下,我可以看到您的建议将是前进的方向。
【解决方案3】:

我会说“最佳实践”是用不同的对象测试一些有意义的东西,因为它将实体存储在列表中。

换句话说,存储类后,类的行为有何不同,并测试该行为。存储是一个实现细节。

话虽如此,但并非总是可以做到这一点。

如果需要,您可以使用reflection

【讨论】:

  • 类行为不同的唯一方式是在调用第二个方法时。这将检查项目列表。检查此行为以查看是否将项目添加到列表中需要在同一测试中测试两种方法。
  • 我不认为必须调用两种方法来进行测试有什么问题,但我更像是一个 TDD/BDD 人而不是测试覆盖人员。我认为替代方案将您的测试与类的实现细节过多地结合在一起,使您的测试成为负担,而不是助手。但理性的人在这一点上持不同意见。
【解决方案4】:

如果我没记错的话,您真正想要测试的是,它仅在可以将项目转换为 IAuditable 时才将项目添加到列表中。因此,您可能会使用方法名称编写一些测试,例如:

  • NotIAuditableIsNotSaved
  • IAuditableInstanceIsSaved
  • IAuditableSubclassInstanceIsSaved

...等等。

问题是,正如您所注意到的,鉴于您的问题中的代码,您只能通过间接方式执行此操作 - 只能通过检查私有 insertItems IList&lt;object&gt; 成员(通过反射或添加属性仅用于测试) 或将列表注入到类中:

public class ClassToBeTested
{
    private IList _InsertItems = null;

    public ClassToBeTested(IList insertItems) {
      _InsertItems = insertItems;
    }
}

那么,测试就很简单了:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     List<object> testList = new List<object>();
     myClassToBeTested     = new MyClassToBeTested(testList);

     // ... create audiableObject here, etc.
     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to testList
}

注入是最具前瞻性和不引人注目的解决方案,除非您有理由认为该列表将成为您的公共界面的一个有价值的部分(在这种情况下,添加一个属性可能会更好 - 当然,属性注入也是完全合法的)。你甚至可以保留一个提供默认实现(new List())的无参数构造函数。

这确实是一个好习惯;考虑到它是一个简单的类,它可能会让您觉得有点过度设计,但仅可测试性就值得了。然后最重要的是,如果你找到了另一个你想使用该类的地方,那将是锦上添花,因为你不会被限制在使用 IList (并不是说稍后进行更改会花费很多精力)。

【讨论】:

  • 我的实际代码中有这三种测试方法,正如您所说,问题是在返回值不可用时检查该项目是否实际添加到列表中。注入列表解决了我的问题,但注入仅用于测试 - 在生产中,列表将完全在类内部,因为它是实现的细节。添加一个纯粹为了测试而注入列表的构造函数是否可以接受?谢谢!
  • 呸,很抱歉在我的回答中如此迂腐 - 在第一次阅读您的问题时,我没有注意到您已经很好地掌握了这些问题。我正在对其进行编辑,使其不那么浮夸,并更好地解决您的核心问题。
  • 注入确实以干净的方式解决了我的问题。但是,我选择通过测试由 OnSave 调用产生的其他方法的行为来避免任何代码更改。 IList 在我的脑海中仍然是一个实现细节,所以我就这样保留了它。同样,在许多情况下,我肯定会使用注入选项。
【解决方案5】:

如果列表是内部实现细节(而且看起来确实如此),那么您不应该对其进行测试。

一个很好的问题是,如果将项目添加到列表中,预期的行为是什么?这可能需要另一种方法来触发它。

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        MyOtherClass other = new MyOtherClass();
        c.Save(other);

        var result = c.Retrieve();
        Assert.IsTrue(result.Contains(other));
    }

在这种情况下,我断言正确的、外部可见的行为是,在保存对象后,它将被包含在检索到的集合中。

如果结果是,在将来,传入的对象应该在某些情况下对其进行调用,那么您可能会有这样的事情(请原谅伪 API):

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        IThing other = GetMock();
        c.Save(other);

        c.DoSomething();
        other.AssertWasCalled(o => o.SomeMethod());
    }

在这两种情况下,您都在测试类的外部可见行为,而不是内部实现。

【讨论】:

    【解决方案6】:

    您需要的测试数量取决于代码的复杂性——大概有多少决策点。不同的算法可以实现相同的结果,但其实现的复杂度不同。您如何编写一个独立于实现的测试,并且仍然确保您有足够的决策点覆盖率?

    现在,如果您正在设计更大的测试,比如说集成级别,那么,不,您不想编写实现或测试私有方法,但问题是针对小的单元测试范围。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-19
      • 2019-09-27
      • 2017-07-23
      • 1970-01-01
      相关资源
      最近更新 更多