单元测试和检查私有变量值

时间:2009-07-07 15:29:13

标签: c# unit-testing nunit

我正在用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的Property添加,或者将List注入ClassToBeTested,但不确定我是否应该修改代码以进行测试。

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

6 个答案:

答案 0 :(得分:85)

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

答案越长,该怎么办?在这种情况下,重要的是要理解为什么实现是这样的(这就是为什么TDD如此强大,因为我们使用测试指定预期的行为,但我感觉你没有使用TDD)。

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

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

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

如果您为无可测量的原因添加了代码,请再次将其删除:)

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

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

答案 1 :(得分:6)

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

如果您真的关心添加,请将其模仿出等式。

编辑:

@TonE:在看完你的评论后,我会说你可能想要改变你当前的OnSave方法,让你知道失败。如果转换失败等,您可以选择抛出异常。然后您可以编写期望和异常的单元测试,而不是。

答案 2 :(得分:2)

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

换句话说,现在它存储了什么行为是不同的类,并测试该行为。存储是一个实现细节。

话虽如此,但并不总是可以这样做。

如果必须,可以使用reflection

答案 3 :(得分:1)

如果我没有弄错的话,你真正想要测试的是,当它们被投射到IAuditable时,它只会将项目添加到列表中。因此,您可以使用以下方法名称编写一些测试:

  • NotIAuditableIsNotSaved
  • IAuditableInstanceIsSaved
  • IAuditableSubclassInstanceIsSaved

......等等。

问题在于,正如您所注意到的,鉴于您的问题中的代码,您只能通过间接执行此操作 - 仅通过检查私有insertItems IList<object>成员(通过反射或仅为了一个目的添加属性)测试)或将列表注入课堂:

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(并不是说以后需要花费太多精力才能进行更改) )。

答案 4 :(得分:1)

如果列表是内部实现细节(似乎是),那么就不应该测试它。

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

    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());
    }

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

答案 5 :(得分:0)

您需要的测试数量取决于代码的复杂程度 - 大致有多少个决策点。不同的算法可以实现相同的结果,其实现具有不同的复杂性。您如何编写独立于实施的测试,并确保您对决策点有足够的报道?

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