我知道所谓的单元测试和集成测试的教科书定义。我好奇的是什么时候该写单元测试了……我将写它们来覆盖尽可能多的类集。

例如,如果我有一个Word类,我将为Word类编写一些单元测试。然后,我开始编写我的Sentence类,当它需要与Word类交互时,我经常会编写单元测试,这样它们就可以同时测试Sentence和Word……至少在他们相互作用的地方。

这些测试本质上变成集成测试了吗?因为它们现在测试这两个类的集成,还是仅仅是一个跨越两个类的单元测试?

一般来说,由于这条不确定的界线,我很少实际编写集成测试……或者我是否使用成品来查看所有部件是否正常工作,即实际的集成测试,即使它们是手动的,并且很少在每个单独的功能范围之外重复?

我是否误解了集成测试,或者集成测试和单元测试之间真的只有很小的区别?


当前回答

此外,重要的是要记住,单元测试和集成测试都可以使用(例如JUnit)自动化和编写。 在JUnit集成测试中,可以使用org.junit.Assume类来测试环境元素(例如,数据库连接)的可用性或其他条件。

其他回答

我也这么做——我把它们都称为单元测试,但在某些时候,我有一个“单元测试”,它涵盖了太多内容,我经常把它重命名为“..IntegrationTest”——只是名字改变了,其他的都没有改变。

我认为从“原子测试”(测试一个微小的类或方法)到单元测试(类级别)和集成测试——然后是功能测试(通常从上到下覆盖了更多的东西)——有一个延续——似乎没有一个干净的分界线。

如果您的测试设置了数据,可能还加载了数据库/文件等,那么它可能更像是一个集成测试(我发现集成测试使用更少的模拟和更多的真实类,但这并不意味着您不能模拟出系统的某些部分)。

对我来说,关键的区别在于,集成测试揭示了一个特性是有效的还是坏的,因为它们在接近现实的场景中强调了代码。它们调用一个或多个软件方法或功能,并测试它们是否按预期工作。

相反,测试单个方法的单元测试依赖于(通常是错误的)软件其余部分正常工作的假设,因为它显式地模拟每个依赖项。

因此,当实现某个特性的方法的单元测试显示为绿色时,并不意味着该特性正在工作。

假设你有一个这样的方法:

public SomeResults DoSomething(someInput) {
  var someResult = [Do your job with someInput];
  Log.TrackTheFactYouDidYourJob();
  return someResults;
}

做某事对你的客户来说非常重要:它是一个特性,是唯一重要的事情。这就是为什么您通常要编写一个Cucumber规范来断言它:您希望验证和交流该特性是否有效。

Feature: To be able to do something
  In order to do something
  As someone
  I want the system to do this thing

Scenario: A sample one
  Given this situation
  When I do something
  Then what I get is what I was expecting for

毫无疑问:如果测试通过了,你就可以断言你交付了一个可以工作的特性。这就是所谓的业务价值。

如果你想为DoSomething写一个单元测试,你应该假装(使用一些模拟)其余的类和方法都在工作(也就是说:方法所使用的所有依赖项都在正确工作),并断言你的方法是工作的。

在实践中,你可以这样做:

public SomeResults DoSomething(someInput) {
  var someResult = [Do your job with someInput];
  FakeAlwaysWorkingLog.TrackTheFactYouDidYourJob(); // Using a mock Log
  return someResults;
}

你可以使用依赖注入,或者一些工厂方法,或者任何模拟框架,或者只是扩展测试中的类。

假设Log.DoSomething()中有一个错误。 幸运的是,Gherkin规范会找到它,您的端到端测试将失败。

这个特性不会工作,因为日志坏了,而不是因为[Do your job with someInput]没有做它的工作。顺便说一下,[Do your job with someInput]是该方法的唯一责任。

同样,假设Log在100个其他特性中使用,在100个其他类的100个其他方法中使用。

是的,100个功能都会失败。但幸运的是,100个端到端测试也失败了,并揭示了这个问题。是的,他们说的是实话。

这是非常有用的信息:我知道我的产品坏了。这也是非常令人困惑的信息:它没有告诉我问题在哪里。它告诉我的是症状,而不是根本原因。

然而,DoSomething的单元测试是绿色的,因为它使用了一个假的日志,被构建为永不崩溃。是的,这显然是在撒谎。它传达了一个坏掉的功能是有效的。它如何有用?

(如果DoSomething()的单元测试失败,请确认:[Do your job with someInput]有一些错误。)

假设这是一个有破碎类的系统:

一个bug就会破坏几个特性,几个集成测试就会失败。

另一方面,相同的错误只会破坏一个单元测试。

现在,比较这两种情况。

同样的错误只会破坏一个单元测试。

使用坏日志的所有功能都是红色的 所有的单元测试都是绿色的,只有Log的单元测试是红色的

实际上,所有使用坏特性的模块的单元测试都是绿色的,因为通过使用模拟,它们删除了依赖项。换句话说,它们运行在一个理想的、完全虚构的世界里。这是唯一隔离和寻找虫子的方法。单元测试意味着模拟。如果你不嘲讽,你就不是单元测试。

的区别

集成测试会告诉你什么是不正常的。但在猜测问题可能在哪里时,它们毫无用处。

单元测试是唯一能告诉您错误确切位置的测试。要绘制此信息,他们必须在模拟环境中运行该方法,在该环境中所有其他依赖项都应该正确工作。

这就是为什么我认为你的句子“或者它只是一个跨越2个类的单元测试”在某种程度上被取代了。一个单元测试不应该跨越2个类。

这个回答基本上是我在这里写的一个总结:单元测试说谎,这就是我喜欢它们的原因。

类比的简单解释

上面的例子已经很好了,我就不再重复了。所以我会用例子来帮助你们理解。

集成测试

集成测试检查是否一切都在一起工作。想象一下手表上的一系列齿轮一起工作。一个综合测试是:手表是否显示正确的时间?它还能在3天内显示正确的时间吗?

它只告诉你整个部件是否正常工作。如果它失败了:它不会告诉你它到底在哪里失败。

单元测试

这些都是非常具体的测试类型。它们告诉你一件具体的事情是有效还是失败。这种类型的测试的关键是,它只测试一个特定的东西,同时假设其他一切都正常工作。这是关键。

例子: 让我们用一个例子来详细说明这一点:

Let’s take a car as an example. Integration test for a car: e.g. does the car drive to Woop Woop and back? If it does this, you can safely say that a car is working from an overall view point. It is an integration test. If it fails you have no idea where it is actually failing: is it the radiator, transmission, engine, or carburettor? You have no idea. It could be anything. Unit test for a car: Is the engine is working? This tests assumes that everything else in the car is working just fine. That way, if this particular unit test fails: you can be very confident that the problem lies in the engine – so you can quickly isolate and fix the problem.

使用存根

假设您的汽车集成测试失败。它不能成功地开到埃丘卡。问题出在哪里? 现在让我们假设你的发动机使用一个特殊的燃油喷射系统,这个发动机单元测试也失败了。换句话说,集成测试和发动机单元测试都失败了。那么问题在哪里呢?(给自己10秒钟的时间来回答。) 是发动机出了问题,还是燃油喷射系统出了问题?

你看到问题了吗?你不知道什么是失败。如果你使用不同的外部依赖关系,那么这10个依赖关系中的每一个都可能导致问题——你不知道从哪里开始。这就是为什么单元测试使用存根来假设其他一切都正常工作。

如果class1的单元测试是测试class1的特性,而class2的单元测试是测试它的特性,并且它们不访问数据库,我想我仍然会把两个相互作用的类称为单元测试。

当一个测试运行了我的堆栈的大部分,甚至到达数据库时,我把它称为集成测试。

我真的很喜欢这个问题,因为TDD讨论有时对我来说有点太纯粹了,看到一些具体的例子对我来说是件好事。

单元测试使用模拟

您正在讨论的是集成测试,它实际上测试了系统的整个集成。但是当你做单元测试时,你应该分别测试每个单元。其他一切都应该被嘲笑。在你的Sentence类的例子中,如果它使用了Word类,那么你的Word类应该被嘲笑。这样,您将只测试您的Sentence类功能。