TDD:为什么让应用程序代码知道它正在接受测试可能是错误的,而不是 运行?

TDD: why might it be wrong to let app code know it is being tested, not run?

this thread 中,布赖恩(唯一的回答者)说 "Your code should be written in such a fashion that it is testing-agnostic"

单个评论说"Your code should definitely not branch on a global "我正在测试标志“。”。

但都没有给出理由,我真的想听听关于此事的一些理性想法。进入给定应用程序 class 并设置一个布尔值表示 "this is a test, not a run".

我发现自己千方百计(注入模拟私有字段等)来实现的各种事情都可以变得更容易实现。

同样明显的是,如果你走得太远,那将是灾难性的......但是作为软件测试库中众多工具中的一种,为什么这个概念会受到如此的谴责?

米克助记词答案:

如果您实际上在方法中间创建一个新的 class 实例并将其分配给私有字段,那么这可能会有帮助的一个简单示例:私有字段模拟将无济于事在那种情况下,因为您正在替换私有字段。但实际上创建一个真实的对象可能会非常昂贵:您可能希望在测试时将其替换为轻量级版本。

我昨天遇到了这样的情况,事实上......我的解决方案是创建一个名为 createXXX() 的新包私有方法......所以我可以模拟它。但这又违背了格言 "thou shalt not create methods just to suit your tests"!

a lot of tests have package-private access to the app classes

我建议不要这样做,在生产代码中破坏封装的想法对我来说就像尾巴摇狗一样。它表明 类 太大和/或缺乏凝聚力。 TDD、依赖注入/控制反转、模拟和编写单一职责 类 应该消除放松可见性的需要。

The single comment says "Your code should definitely not branch on a global "am I being tested flag".".

生产代码就是生产代码,不需要了解您的测试。那里不应该有关于测试的逻辑,它的分离很差。同样,依赖注入/控制反转将允许您在运行时交换特定于测试的逻辑,这不会包含在生产工件中。

我会将这个答案分成两部分。首先我会分享我对 Brian 的回答的看法,然后我会分享一些关于如何有效测试的技巧。

Brian 回答的解释

Brian 似乎暗示了两个关键想法。我将逐一解决。

想法 1:生产代码不应依赖于测试

Your code should be written in such a fashion that it is testing-agnostic.

生产代码应该依赖于测试。应该是反过来的。

这有多种原因:

  1. 更改测试 不会更改代码的行为
  2. 您的生产代码可以独立于测试代码进行编译和部署
  3. 更新测试时,您的代码不需要重新编译
  4. 您的生产代码 不可能因非 运行 测试代码的意外副作用而失败

注意:任何体面的编译器都会删除测试代码。虽然我不认为这是 design/test 你的系统不佳的借口。

想法 2:您应该测试抽象而不是实现

Whatever environment you test in should be as close to real-world as possible.

听起来 Brian 可能在他的回答中暗示了这个想法。与上一个想法不同,这个想法并未得到普遍认可,因此请持保留态度。

通过测试抽象,您可以培养对被测试单元的尊重程度。您同意您将不会四处查看其内部结构并监视其内部状态。

Why shouldn't I spy on the state of objects during testing?

通过监视 object 的内部结构,您导致了这些问题:

  1. 您的测试将将您绑定到单元的特定实现

    例如...
    想要更改 class 以使用不同的排序算法?太糟糕了,你的测试将失败,因为你断言 quicksort 函数 必须 被调用。

  2. 您将破坏封装

    通过测试 object 的内部状态,您会很想放松 object 的一些隐私。这将意味着您的更多 生产 代码也将提高您的 object.

    的可见性

    通过放松 object 的封装,您正在诱使其他生产代码也依赖于它。这不仅可以将您的 测试 与特定的实现联系起来,而且还可以将您的整个系统本身联系起来。你希望这种情况发生。

Then how do I know if the class works?

测试被调用方法的pre-conditions和post-conditions/results。如果您需要更复杂的测试,请查看我写的关于 mocking 和依赖注入的最后一节。

迷你笔记

我认为在您的主要方法中使用 if (TEST_MODE) 不一定是坏事 只要您的生产代码 保持独立于您的测试。

例如:

public class Startup {

    private static final boolean TEST_MODE = false;

    public static void main(String[] args) {
        if (TEST_MODE) {
            TestSuite testSuite = new TestSuite();
            testSuite.execute();
        } else {
            Main main = new Main();
            main.execute();
        }
    }
}

但是,如果您的其他 class 知道 他们 运行 处于测试模式,就会出现问题。如果您的所有生产代码中都有 if (TEST_MODE),那么您就会面临我上面提到的问题。

显然在 Java 中你会使用 JUnit 或 TestNG 之类的东西而不是这个,但我只是想分享我对 if (TEST_MODE) 想法的看法。

如何进行有效测试

这是一个很大的话题,所以我会尽量缩短这部分的答案。

  • 不监视内部状态,使用模拟和依赖注入

    使用模拟,您可以断言您注入的模拟方法已被调用。更好的是,依赖注入将反转你的 classes 对你注入的任何实现的依赖。这意味着您可以交换事物的不同实现而无需担心。

    这完全消除了在您的 classes 中四处走动的需要。


如果有一本书我强烈推荐阅读,那就是 Modern C++ Programming with Test-Driven Development by Jeff Langr。这可能是我用过的最好的 TDD 资源。

尽管标题中有 C++,但它的主要重点肯定是 TDD。这本书的介绍谈到了这些例子应该如何适用于所有(相似的)语言。鲍勃叔叔甚至在前言中这样说:

Do you need to be a C++ programmer to understand it? Of course you don't. The C++ code is so clean and is written so well and the concepts are so clear that any Java, C#, C, or even Ruby programmer will have no trouble at all.

想想大众汽车的大丑闻。在测试下与在生产负载下表现不同的系统并没有真正经过测试。也就是说:它实际上是 两个 系统,即生产系统和测试系统 - 其中唯一经过测试的是测试系统。不同的生产系统未经过测试。您在两个系统之间引入的每一个行为差异都是一个测试漏洞。

TDD: why might it be wrong to let app code know it is being tested, not run?

1) Carl Manaster 带来了一个优秀而简短的答案。如果您的实施根据是否经过测试而具有不同的行为,则您的测试没有价值,因为它没有反映生产中应用程序的真实行为,因此它没有验证要求。

2) Test-Driven 开发与让应用程序代码知道它正在测试的事实无关。无论您使用何种开发方法,都可能会引入此类错误。

根据我的 TDD 经验,我认为 TDD 不会让应用程序代码知道它正在被测试,因为当您一开始就编写单元测试并且您适当地执行它时,您可以保证拥有一个自然可测试的应用程序验证应用程序要求并且不了解测试代码的代码。

我想,当您在编写应用代码后创建测试代码时,这种错误更有可能发生,因为您可能不想重构应用代码以使您的代码可测试,因此添加一些技巧在实现中绕过重构任务。

3) Test-Driven 开发是有效的代码,但您不能忘记应用程序 class 和测试 class 的设计方面。

A trivial example of how this might help would be if you're actually creating a new class instance in the middle of a method and assigning it to a private field: private field mocks won't help in that case because you are replacing the private field. But actually creating a real object might be very costly: you might want to replace it with a lightweight version when testing.

I encountered such a situation yesterday, in fact... and my solution was to create a new package-private method called createXXX()... so I could mock it. But this in turn goes against the dictum "thou shalt not create methods just to suit your tests"!

在某些情况下使用 package-private 修饰符是可以接受的,但只有在设计代码的所有自然方法都不允许有可接受的解决方案时才应使用它。

"thou shalt not create methods just to suit your tests" may be misleading.

事实上我更愿意说:"thou shalt not create methods to suit your tests and that open the API of the application in an undesirable way"

在您的示例中,当您想要修改代码的依赖项时,您希望在测试期间模拟或替换依赖项,如果您练习 TDD,则不应直接修改实现,而应从测试代码开始修改.
如果你的测试代码似乎被阻塞了,因为你错过了一个构造函数、一个方法、一个对象等等……来设置对你测试的 class 的依赖,你将被迫添加你的测试 class。
就是TDD方式。

上面,我提到不要打开 API 超过需要。 我将给出两个示例,它们提供了一种设置依赖项的方法,但不会以相同的方式打开 API。

这种做法是可取的,因为客户端无法在生产中更改 MyClass 的行为:

@Service
public class MyClass{
...
MyDependency myDependency;
...
@Autowired
public MyClass(MyDependency myDependency){
   this.myDependency = myDependency;
}
 ...
}

这种做法不太理想,因为 MyClass API 在应用代码不需要它时增长。除了使用这种新方法外,客户端还可以使用 myDependency 字段的 setter 更改生产中 MyClass 的行为:

@Service
public class MyClass{
...
MyDependency myDependency;
...
@Autowired
public void setMyDependency(MyDependency myDependency){
   this.myDependency = myDependency;
}
 ...
}

请注意:如果构造函数中的参数超过 4 或 5 个,使用起来可能会很麻烦。
如果发生这种情况,使用 setters 可能仍然不是最好的解决方案,因为问题的根源可能是 class 有太多责任。所以如果是这种情况应该重构。

我仔细阅读了所有这些答案,它们都很有帮助。但也许我应该重新 class 证明自己:我似乎正在成为一个 low-intermediate TDD 从业者,而不是一个新手。在过去 6 个月左右的时间里,我已经吸收了很多这些要点和经验法则,无论是通过阅读还是有时莫名其妙,偶尔痛苦但总是有启发性的经验。

卡尔·马纳斯特 (Carl Manaster) 与大众汽车丑闻的类比很诱人,但可能有点不适用:我并不是在建议应用程序代码应该 "detect" 正在进行测试并因此改变其行为。

的建议是,存在一两个棘手、麻烦的 low-level 问题,您可能希望以不干扰的方式使用此工具以任何方式使用 TDD 的 cast-iron 规则和 "philosophy"。

两个例子:

我的代码中有一些情况会抛出异常,而我想检查它们是否被抛出的测试。很好:我去 doThrow( ... )@Test( expected = ... ) 一切正常。但是在生产过程中 运行 我想要打印出一条带有堆栈跟踪的错误消息。在测试期间 运行 我只想要错误消息。我不希望 logback-test.xml 完全抑制 error-level 日志记录。但显然没有办法配置记录器来防止打印出堆栈跟踪。

所以我能做的就是在应用程序代码中使用这样的方法,专为测试而设计:

boolean suppressStacktrace(){ return false; };

... 然后我将其用作给定 LOGGER.error( ... 情况的测试,然后当我想在期间引发该异常时将该方法模拟为 return true测试。

其次,控制台输入的具体情况:BufferedReader.readLine()。将另一个 InputStream 替换为 System.in 并用不同 StringsList 喂养它,每个 readLine 将提供一次,这在谚语中是正确的痛苦。我所做的是在应用 class:

中有一个 private 字段
Deque<String> inputLinesDeque;

... 和一个 package-private 方法用 List<String> 输入行设置它,然后可以 popped 直到 Deque 为空。在应用 运行 期间,此 Dequenull,因此 if 分支到 br.readline()

这只是两个例子。毫无疑问,在其他情况下,ultra-purist 方法的代价太高,而且可以说不会带来任何实际好处。

但是,我很欣赏 davidxxx 对 TDD 10 诫命之一的卓越定义:"thou shalt not create methods to suit your tests and that open the API of the application in an undesirable way"。很有帮助:深思。

以后

自从一个月前写这篇文章以来,我意识到扩展和修改 logback classes 远非不可能......我认为自己制作并不会太难logback class 确实会接受 logback-test.xml 到 "supress stack traces" 中的配置标志。当然,当你为你的应用程序制作可执行 jar 时,这个定制的 logback class 不必导出......但同样,对我来说这属于 "jumping through hoops" 的类别. "pure" 应用代码真的需要怎样?