如何测试访问私有成员的公共方法?

3

所以我有类似于这样的东西

private List<ConcurrentQueue<int>> listOfQueues = new List<ConcurrentQueue<int>>()
public void InsertInt(int id, int value)
{
    listOfQueues[id].Enqueue(value);
}

这是不应进行单元测试的内容吗?
如果是的话,我该如何测试InsertInt方法而不使用其他任何方法呢? 可以使用该方法从队列中检索数据来测试是否正确输入吗? 这些内容是否应该使用mocks来处理?

6个回答

2
是的,您应该对其进行单元测试。您可以使用私有访问器来获取listOfQueues。您必须通过单元测试确保该方法在出现异常时表现符合预期,并确保项目真正被插入。
请查看这篇文章,了解如何对私有方法进行单元测试 http://msdn.microsoft.com/en-us/library/ms184807(v=vs.80).aspx

按照所列出的文章操作将导致 Visual Studio 询问是否需要向程序集添加私有访问器。您可以通过在 AssemblyInfo.cs 页面中添加以下行来手动执行此操作 [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("<testing assembly 的名称>")]。 - JMcCarty
InternalsVisibleTo只能让internal成员可见,而不能让private成员可见。上述文章会在私有方法周围生成反射包装器,您可以将其编译到测试程序集中。 - James Gaunt
有没有一种方法可以为成员生成私有访问器而不是方法? - scott
@scott 我还没有测试过,但是请查看这个链接 http://msmvps.com/blogs/deborahk/archive/2009/10/29/unit-testing-exposing-private-members.aspx - Oskar Kjellin

2
通常不建议测试私有成员。单元测试的目的是,从外部看,类接受指定的输入,并(在请求时)给出正确的输出。您不关心它如何提供这些输出的实现方式,只要它为外部使用提供了正确的输出即可。
举个例子,假设您创建了一个单元测试来验证您的InsertInt()方法是否将int插入到listOfQueues中。然后您的需求发生了改变,您必须更改实现策略,而不是使用List<ConcurrentQueue<int>>,而是使用Dictionary<string, ConcurrentQueue<int>>。这可能实际上不需要更改您的输入,您的输出仍然可以通过任何输出验证,但是您的InsertInt()单元测试将失败,因为它被硬编码到实现中。
更好的做法是进行单元测试,确保如果您使用正确的输入调用InsertInt(),则您的输出方法仍将返回正确的输出,并创建单元测试以检查使用无效参数调用InsertInt()是否会导致异常。然后,您可以更改内部实现的所有内容,并确信您的类仍然正常工作。测试实现会增加额外的开销,而在可测试性方面提供很少的好处。
请注意,我并不是说这种方法不应该进行单元测试,而是单元测试需要以反映外部对象如何与您的类交互的方式开发。

那么,将多个测试放在一个单元测试中进行测试,比将单元测试与实现绑定在一起更好吗? - scott
是的,我相信这样做是因为它完全模拟了该类在实际使用场景中的使用方式。如果您独立测试这两种方法,您就没有单元测试可以确保输入和输出方法实际上是一起工作的,这只是一种假设。然后,您可以更改一个实现(和测试),尽管您的单元测试将通过,但代码将在生产中失败。您仍然需要进行单元测试,以确保如果您使用x参数调用输入方法,输出将为y参数,因此私有测试是浪费时间和精力。 - KallDrexx
此外,这仍然是单元测试,因为你的单元测试只处理了该类本身,并没有涉及到类之外的部分,因此处理多个方法并不是一件坏事。 - KallDrexx

1

由于此方法是公共方法,因此需要进行单元测试。

快速查看您的方法会发现将-1作为id的值传递将导致ArgumentOutOfRangeException。如果为该方法设计了单元测试用例(假设存在许多这样的方法),则在编码期间将会意识到这一点。

要检查插入是否成功,可以使用@Oskar Kjellin指出的方法。

如果您想要深入了解,那么可以使用Reflection来检查值是否已被插入。

// Sample with Queue instead of ConcurrentQueue
private void TestInsertInt(int id, int value)
{
    myInstance.InsertInt(id, value);

    FieldInfo field = myInstance.GetType().GetField("listOfQueues", BindingFlags.NonPublic | BindingFlags.Instance);
    List<Queue<int>> listOfQueues = field.GetValue(myInstance) as List<Queue<int>>;
    int lastInserted = listOfQueues[id].Last();

    Assert.AreEqual(lastInserted, value);
}

我也有同感。所以我想更重要的问题是什么是正确的做法? - scott
你不需要对私有成员进行单元测试,只需创建一个单元测试,传入零的ID将导致 ArgumentOutOfRangeException - KallDrexx

1

你不应该测试队列的行为 - 这是一个实现细节,你可以在不改变方法行为的情况下进行更改。例如,你可以用另一种数据结构(比如树)替换ConcurrentQueue,而无需更新单元测试。

你要测试的是这个方法是否按照你的期望接受输入并存储值。因此,你需要一些方式来查询系统状态,例如

public int GetInt(int id)

然后,您可以通过使用相同的 id 检索结果来测试该方法是否按照您的预期插入。

您应该测试公共方法在每种情况下返回您预期的结果,并将存储值的方式留给方法自行处理。因此,我可能会使用不同的输入来测试该方法:

        [TestCase(1,2,3)] // whatever test cases make sense for you
        [TestCase(4,5,6)]
        [TestCase(7,8,9)]
        [Test]
        public void Test_InsertInt_OK( int testId, int testValue, int expectedValue)
        {
            InsertInt(testId, testValue);
            Assert.AreEqual( GetInt(testId), expectedValue )
        }

0
这是我不应该进行单元测试的东西吗?
如果你的目标是100%的测试覆盖率,那么是的。
如果是这样,我如何在不使用任何其他方法的情况下测试InsertInt方法?
有几个选项:
- 使用不同的访问器方法,就像你建议的那样。 - 将容器设置为内部,并允许单元测试程序集访问内部。 - 将容器分解成一个单独的类,将其作为依赖项传递给此类,然后在单元测试中传递一个模拟容器。 - 使用反射访问私有成员。

0

另一个技巧是使用PrivateObject

个人而言,我要么使用DI(依赖注入),要么将privateinternal用于单元测试的目的!


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