TDD: 您为单元测试公开了哪些方法?

17

有关TDD的一个方面我从未完全理解。

假设有人要求你实现一个简单的堆栈对象。如果你已经正确设计,你会得到一个非常简洁和干净的 API。例如: push()pop()isEmpty()。任何超出这些范围的东西都是过度实现需求,让用户有太多余地来操作你的代码。

那么现在假设你想对你的代码进行单元测试。如果你所有的公共方法只是上面展示的三个方法,你该如何去做呢?这些方法只能帮助你进行有限的测试。

因此,你可以添加私有方法,但由于它们对你的单元测试用例不可见,所以并没有任何帮助。或者你将这些方法变成公共方法,那么你费尽心思设计的简约API也就毁了。现在用户将会操纵你的堆栈,错误也随之而来。

你如何处理这种打开公共方法以进行测试与拥有简洁而简单的API之间的困境?

编辑: 为了指明正确的方向,获取技术指针(如“使用此技巧公开私有方法”等等)会很不错,但我更感兴趣的是更通用的答案,例如哪个概念更重要以及你如何处理这个问题。


这是同一主题的另一个问题:你如何对私有方法进行单元测试? - Don Kirkby
9个回答

15
  1. 测试功能;这通常意味着测试公共接口 - 因为所有的功能都应该通过公共接口访问,否则它们就不是功能!也许有例外情况,但我想不到任何例外情况。

  2. 测试公共接口;任何没有直接或间接从公共接口调用的方法都是不必要的。不仅它们不需要被测试,它们根本不需要存在。


1
我建议这样澄清:如果你开始感到需要暴露一些私有的东西进行测试,不要这样做,而是把它看作是你的实现过于复杂,需要重新考虑实现的粒度和测试。 - Rob Williams

6

您应该看一下这个问题:你测试私有方法吗?

为了不破坏封装性,如果我发现私有方法非常庞大、复杂或者非常重要,需要单独进行测试,我会将其放在另一个类中并在那里公开(方法对象)。然后,我可以轻松地测试以前是私有的但现在是公共的方法,它现在存在于自己的类中。


4

使用你的堆栈示例,你真的不需要暴露任何内部工作来进行单元测试。重申其他人已经说过的,你可以随意使用尽可能多的私有方法,但是只能通过公共API进行测试。

[Test]
public void new_instance_should_be_empty()
{
  var stack = new Stack();

  Assert.That(stack.IsEmpty(), Is.True);
}

[Test]
public void single_push_makes_IsEmpty_false()
{
  var stack = new Stack();

  stack.Push("first");

  Assert.That(stack.IsEmpty(), Is.False);
}

使用推入(push)和弹出(pop)的组合方式,您可以模拟Stack类的所有行为,因为这是用户与之交互的唯一方式。您不需要任何技巧,因为您只需要测试其他人能使用的内容。
[Test]
public void three_pushes_and_three_pops_results_in_empty_stack()
{
  var stack = new Stack();

  stack.Push("one");
  stack.Push("two");
  stack.Push("three");
  stack.Pop();
  stack.Pop();
  stack.Pop();

  Assert.That(stack.IsEmpty(), Is.True);
}

我更喜欢这个答案,因为这个代码示例非常好地展示了你的测试应该专注于API和结果,而不是实现细节! - sara

2
有时我会将原本应该是私有的方法转换成包级别的(Java)或者内部的(.NET)方法,通常会加上注释或者注解/属性来解释为什么这样做。
然而,大多数情况下,我会避免这样做。公共API在你的堆栈案例中不能测试什么?如果用户可以看到错误并且他们只使用公共API,那么什么阻止你进行相同的调用呢?
我公开原本为私有的方法的时候是为了让测试一组复杂步骤中的某一部分变得更容易 - 如果单个方法调用非常“粗糙”,那么逐步测试每个步骤就会非常有帮助,即使这些步骤对于普通用户不可见。

2

正确的TDD是关于测试可以通过公共接口测试的行为... 如果有任何私有方法,则这些方法应该通过任何公共接口间接测试...


2

采用测试驱动开发(TDD)的方式,你的所有代码都应该能够从公共接口中访问:

  • 首先编写功能测试。
  • 然后编写最少量的代码以使测试通过。这表明你已经实现了功能。

如果有一些代码没有被覆盖,那么这意味着这些代码可能是无用的(删除它并重新运行测试),或者你的测试不完整(某些功能已经实现但没有被测试显式地识别出来)。


0

你可以使用反射来测试私有方法,但是以后会很麻烦。我认为你需要将测试放在同一个程序集(或包)中,并尝试使用 Internal。这样你就有了一些保护,可以测试你想要测试的东西。


0
如果您的私有方法没有被公共API测试方法间接测试,并且需要进行测试,则我建议将主类委托给另一个辅助类。
public Stack {
    public ... push(...) {...}
    public ... pop(...) {...}
    public ... isEmpty(...) {...}

    // secondary class
    private StackSupport stackSupport;
    public StackSupport getStackSupport() {...}
    public void setStackSupport(StackSupport stackSupport) {...}
}

public StackSupport {
    public ...yourOldPrivateMethodToTest(...) {...}
}

那么你的私有方法就成为了另一个类中的公共方法。你可以在另一个类的测试中测试这个公共方法。:-)


-1

在使用TDD编码时,我为对象创建公共接口API。这意味着在您的示例中,我的接口只包含push()pop()isEmpty()

然而,通过调用它们来测试这些方法本身并不是单元测试(有关此问题的更多信息请参见本文末尾),因为它们很可能测试多个内部方法的协作,这些方法一起产生所需的结果,这就是您的问题:这些方法应该是私有的吗?

我的答案是否定的,应该使用protected(或您选择的语言中的等效项)代替private,这意味着如果您的项目和测试POM的结构类似,则测试套件类可以查看实际类的内部,因为它们实际上位于同一个文件夹中。这些可以被视为单元测试:您正在测试类本身的功能块,而不是它们的协作。

关于测试单个接口/API方法,当然也很重要,我不会争辩这一点,但是它们介于单元测试验收测试的模糊线之间。

在我看来,最重要的是要记住单元测试可以告诉你一个方法是否行为异常,验收测试可以告诉你多个方法/类的协作是否异常,而集成测试则可以告诉你多个系统的协作是否异常。


你使用的“单元”定义是什么,可以使私有方法符合条件?我看到的大多数定义都谈论“公共方法”或“公共类上的离散方法集”,或更抽象地说,“一段代码,以这样的方式完成实际任务,以便您可以针对某些具体结果(状态更改,实际返回值,外部调用等)进行断言。” - sara

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