28

最近,我们一直在向现有的 Java 应用程序添加自动化测试。

我们有什么

这些测试中的大多数是集成测试,它可能涵盖一系列调用,例如:-

  1. HTTP 发布到 servlet
  2. servlet 验证请求并调用业务层
  3. 业务层通过休眠等做了很多事情并更新了一些数据库表
  4. servlet 生成一些 XML,通过 XSLT 运行它以生成响应 HTML。

然后,我们验证 servlet 以正确的 XML 响应,并且数据库(我们的开发 Oracle 实例)中存在正确的行。然后删除这些行。

我们还有一些较小的单元测试来检查单个方法调用。

这些测试都作为我们夜间(或临时)构建的一部分运行。

问题

这看起来不错,因为我们正在检查系统的边界:一端是 servlet 请求/响应,另一端是数据库。如果这些工作,那么我们可以自由地重构或弄乱中间的任何东西,并对被测 servlet 继续工作有一定的信心。

这种方法可能会遇到什么问题?

我看不出在单个类上添加更多单元测试会有什么帮助。这不会使重构变得更加困难,因为我们更有可能需要丢弃并重新编写测试?

4

12 回答 12

34

单元测试更紧密地定位故障。集成级测试更贴近用户需求,因此可以更好地预测交付成功。除非构建和维护,否则它们都不是很好,但如果使用得当,它们都非常有价值。


(更多的...)

单元测试的问题是,没有任何集成级别测试可以像一组好的单元测试那样对所有代码进行测试。是的,这可能意味着您必须对测试进行一些重构,但总的来说,您的测试不应过多地依赖于内部结构。因此,例如,假设您有一个函数来获得 2 的幂。你描述它(作为一个正式的方法人,我声称你指定它)

long pow2(int p); // returns 2^p for 0 <= p <= 30

您的测试和您的规范看起来基本相同(这是一种伪 xUnit 用于说明):

assertEqual(1073741824,pow2(30);
assertEqual(1, pow2(0));
assertException(domainError, pow2(-1));
assertException(domainError, pow2(31));

现在您的实现可以是一个带有多个的 for 循环,您可以稍后再将其更改为一个班次。

如果您更改实现,例如,它返回 16 位(请记住,sizeof(long)仅保证不小于sizeof(short)),那么此测试将很快失败。集成级别的测试可能会失败,但不一定,而且在pow2(28).

关键是他们确实测试了不同的情况。如果您可以构建足够详细的集成测试和广泛的集成测试,那么您可能能够获得相同水平的覆盖率和细粒度测试,但这可能很难做到,而且指数级的状态空间爆炸会打败您。通过使用单元测试对状态空间进行分区,您需要的测试数量比指数增长要少得多。

于 2009-04-21T04:23:19.373 回答
29

您在问两种不同事物的利弊(骑马与骑摩托车的利弊是什么?)

当然,两者都是“自动测试”(~riding),但这并不意味着它们是替代品(你不会骑马数百英里,也不会在封闭车辆的泥泞中骑摩托车地方)


单元测试测试代码的最小单元,通常是一个方法。每个单元测试都与它正在测试的方法密切相关,如果它写得很好,它(几乎)只与它相关。

它们非常适合指导新代码的设计和现有代码的重构。在系统准备好进行集成测试之前,它们很适合发现问题。请注意,我写了指南,所有的测试驱动开发都是关于这个词的。

进行手动单元测试没有任何意义。

重构呢?这似乎是您主要关心的问题?如果你只是重构一个方法的实现(内容),而不是它的存在或“外部行为”,那么单元测试仍然是有效的并且非常有用(在你尝试之前你无法想象有多大用处)。

如果您正在更积极地重构,改变方法的存在或行为,那么是的,您需要为每个新方法编写一个新的单元测试,并可能丢弃旧的。但是编写单元测试,特别是如果你在代码本身之前编写它,将有助于阐明设计(即方法应该做什么,不应该做什么而不会被实现细节(即方法应该如何做它需要做的事情)。


自动化集成测试测试代码的最大单元,通常是整个应用程序。

它们非常适合测试您不想手动测试的用例。但是您也可以进行手动集成测试,它们同样有效(只是不太方便)。


今天开始一个新项目,没有单元测试没有任何意义,但我想说,对于像你这样的现有项目,为你已经拥有的所有东西编写它们并没有多大意义,而且它正在工作。

在你的情况下,我宁愿使用“中间立场”的方法写作:

  1. 较小的集成测试,仅测试您要重构的部分。如果您要重构整个事情,那么您可以使用您当前的集成测试,但是如果您只是重构 - 比如说 - XML 生成,那么要求数据库的存在没有任何意义,所以我会写一个简单而小型的 XML 集成测试。
  2. 一堆你要写的新代码的单元测试。正如我在上面已经写过的,一旦你“弄乱了中间的任何东西”,单元测试就会准备好,确保你的“乱七八糟”在某个地方。

实际上,您的集成测试只会确保您的“混乱”不起作用(因为一开始它不会起作用,对吗?)但它不会给您任何线索

  • 为什么它不工作
  • 如果您对“混乱”的调试确实在修复某些问题
  • 如果您对“混乱”的调试正在破坏其他东西

如果整个更改成功,集成测试只会在最后给出确认(答案将很长一段时间都是“否”)。在重构过程中,集成测试不会给您任何帮助,这会使重构变得更加困难并且可能令人沮丧。您需要为此进行单元测试。

于 2009-05-04T20:02:29.417 回答
20

我同意 Charlie 关于更多与用户操作和整个系统的正确性相对应的集成级测试。我确实认为单元测试比仅仅更紧密地定位失败更有价值。单元测试为集成测试提供了两个主要价值:

1) 编写单元测试与测试一样是一种设计行为。如果你练习测试驱动开发/行为驱动开发,那么编写单元测试的行为可以帮助你准确地设计你的代码应该做什么。它可以帮助您编写更高质量的代码(因为松散耦合有助于测试),它可以帮助您编写足够的代码以使您的测试通过(因为您的测试实际上是您的规范)。

2) 单元测试的第二个价值是,如果它们编写得当,它们会非常非常快。如果我对您项目中的一个类进行更改,我可以运行所有相应的测试来查看我是否破坏了任何东西吗?我怎么知道要运行哪些测试?他们需要多长时间?我可以保证它会比写得很好的单元测试更长。您最多应该能够在几分钟内运行所有单元测试。

于 2009-04-21T12:07:16.873 回答
16

仅举几个亲身经历的例子:

单元测试:

  • (+) 使测试接近相关代码
  • (+) 相对容易测试所有代码路径
  • (+) 很容易看出是否有人无意中改变了方法的行为
  • (-) UI 组件比非 GUI 更难编写

集成测试:

  • (+) 在项目中拥有具体细节固然很好,但是集成测试可以确保它们彼此匹配
  • (-) 更难定位错误源
  • (-) 更难测试所有(甚至所有关键)代码路径

理想情况下,两者都是必要的。

例子:

  • 单元测试:确保输入索引 >= 0 并且 < 数组长度。越界时会发生什么?方法应该抛出异常还是返回null?

  • 集成测试:当输入负库存值时,用户会看到什么?

第二个影响 UI 和后端。双方都可以完美地工作,你仍然可能得到错误的答案,因为两者之间的错误条件没有明确定义。

我们发现关于单元测试的最好的部分是它使开发人员从代码->测试->思考到思考->测试->代码。如果开发人员必须先编写测试,[s] 他往往会更多地考虑预先可能出现的问题。

要回答您的最后一个问题,由于单元测试与代码如此接近并迫使开发人员提前思考更多,在实践中我们发现我们不倾向于重构代码,因此更少的代码被移动- 所以不断地折腾和编写新的测试似乎不是问题。

于 2009-05-04T13:22:36.303 回答
5

这个问题肯定有哲学部分,但也指向务实的考虑。

测试驱动设计用作成为更好的开发人员的手段有其优点,但这不是必需的。许多优秀的程序员从未编写过单元测试。进行单元测试的最佳理由是它们在重构时赋予您的力量,尤其是当许多人同时更改源代码时。在签入时发现错误也可以为项目节省大量时间(考虑迁移到 CI 模型并基于签入而不是每晚构建)。因此,如果您在编写它测试的代码之前或之后编写单元测试,那么您在那一刻就可以确定您编写的新代码。单元测试可以确保以后该代码可能发生的事情 - 这可能很重要。单元测试可以在进行 QA 之前阻止错误,从而加快您的项目。

如果正确完成,集成测试会强调堆栈中元素之间的接口。以我的经验,集成是项目中最不可预测的部分。让各个部分发挥作用往往并不难,但将所有内容放在一起可能非常困难,因为在此步骤中可能会出现各种错误。在许多情况下,项目延迟是因为集成中发生的事情。在此步骤中遇到的一些错误是在接口中发现的,这些接口已被一侧所做的某些更改破坏,而这些更改未传达给另一侧。集成错误的另一个来源是在 dev 中发现但在应用程序进入 QA 时被遗忘的配置。集成测试可以帮助显着减少这两种类型。

可以对每种测试类型的重要性进行辩论,但对您来说最重要的是将任何一种类型应用于您的特定情况。有问题的应用程序是由一小群人还是由许多不同的群体开发的?您是否有一个存储库来存储所有内容,或者每个应用程序的特定组件都有多个存储库?如果您有后者,那么您将面临不同组件的不同版本的相互兼容性的挑战。

每种测试类型都旨在暴露开发阶段不同集成级别的问题,以节省时间。单元测试推动了许多开发人员在一个存储库上操作的输出的集成。集成测试(名字不好)推动了堆栈中组件的集成——组件通常由不同的团队编写。集成测试暴露的问题类别通常更耗时修复。

所以务实地说,它真的归结为你在自己的组织/流程中最需要速度的地方。

于 2009-05-02T11:54:27.907 回答
3

单元测试和集成测试的区别在于运行测试所需的部件数量。

单元测试(理论上)需要非常(或不需要)其他部分来运行。集成测试(理论上)需要运行很多(或所有)其他部分。

集成测试测试行为和基础设施。单元测试通常只测试行为。

因此,单元测试适用于测试某些东西,集成测试适用于其他东西。

那么,为什么要进行单元测试呢?

例如,在集成测试时很难测试边界条件。示例:后端函数需要一个正整数或 0,前端不允许输入负整数,当您将负整数传递给后端函数时,如何确保其行为正确?也许正确的行为是抛出异常。这在集成测试中很难做到。

因此,为此,您需要一个(函数的)单元测试。

此外,单元测试有助于消除集成测试期间发现的问题。在上面的示例中,单个 HTTP 调用有很多故障点:

来自 HTTP 客户端的调用 servlet 验证 从 servlet 到业务层的调用 业务层验证 数据库读取(休眠)业务层的数据转换 数据库写入(休眠)数据转换 -> XML XSLT 转换-> HTML HTML 的传输 -> 客户端

为了使您的集成测试正常工作,您需要所有这些过程才能正常工作。对于 servlet 验证的单元测试,您只需要一个。servlet 验证(可以独立于其他所有内容)。一层中的问题变得更容易追踪。

您需要单元测试和集成测试。

于 2009-05-04T15:19:45.087 回答
2

单元测试在类中执行方法来验证正确的输入/输出,而无需在应用程序的更大上下文中测试类。您可以使用模拟来模拟依赖类——您正在将类作为独立实体进行黑盒测试。单元测试应该可以从开发人员工作站运行,而无需任何外部服务或软件要求。

集成测试将包括您的应用程序和第三方软件的其他组件(例如,您的 Oracle 开发数据库或 Web 应用程序的 Selenium 测试)。这些测试可能仍然非常快并且作为持续构建的一部分运行,但是因为它们注入了额外的依赖项,所以它们也有注入新错误的风险,这些错误会导致您的代码出现问题但不是您的代码引起的。最好,集成测试也是您注入真实/记录数据并断言应用程序堆栈作为一个整体在给定这些输入的情况下按预期运行的地方。

问题归结为您要查找什么样的错误以及您希望以多快的速度找到它们。单元测试有助于减少“简单”错误的数量,而集成测试可以帮助您找出架构和集成问题,希望能模拟墨菲定律对整个应用程序的影响。

于 2009-04-29T06:27:31.460 回答
2

Joel Spolsky 写了一篇关于单元测试的非常有趣的文章(这是 Joel 和其他人之间的对话)。

主要思想是单元测试是非常好的事情,但前提是你以“有限”的数量使用它们。Joel 不建议在 100% 的代码都在测试用例下时达到状态。

单元测试的问题在于,当您想要更改应用程序的架构时,您必须更改所有相应的单元测试。而且它会花费很多时间(甚至可能比重构本身还要多)。在所有这些工作之后,只有少数测试会失败。

因此,只为确实会造成一些麻烦的代码编写测试。

我如何使用单元测试:我不喜欢 TDD,所以我首先编写代码,然后测试它(使用控制台或浏览器),以确保此代码能够正常工作。只有在那之后,我才添加“棘手”的测试——其中 50% 的测试在第一次测试后失败。

它有效,并且不需要太多时间。

于 2009-05-04T15:34:45.487 回答
2

我们的项目中有 4 种不同类型的测试:

  1. 必要时使用模拟进行单元测试
  2. 行为类似于单元测试的数据库测试,但之后会触及数据库并进行清理
  3. 我们的逻辑通过 REST 公开,因此我们有执行 HTTP 的测试
  4. 使用实际使用 IE 实例并检查主要功能的 WatiN 进行 Webapp 测试

我喜欢单元测试。它们运行得非常快(比 #4 测试快 100-1000 倍)。它们是类型安全的,因此重构非常容易(使用良好的 IDE)。

主要问题是需要做多少工作才能正确完成它们。您必须模拟所有内容:数据库访问、网络访问、其他组件。你必须装饰不可修改的类,得到无数几乎无用的类。您必须使用 DI,这样您的组件就不会紧密耦合,因此不可测试(请注意,使用 DI 实际上并不是一个缺点:)

我喜欢测试#2。他们确实使用数据库并且会报告数据库错误、违反约束和无效列。我认为我们可以使用它进行有价值的测试。

#3,尤其是#4更有问题。它们需要构建服务器上的一些生产环境子集。您必须构建、部署并让应用程序运行。你每次都必须有一个干净的数据库。但最终,它得到了回报。Watin 测试需要不断的工作,但您也需要不断的测试。我们在每次提交时运行测试,当我们破坏某些东西时很容易看到。

所以,回到你的问题。单元测试很快(这非常重要,构建时间应该少于 10 分钟)并且很容易重构。如果您的设计发生变化,这比重写整个事情要容易得多。如果您使用具有良好查找用法命令的漂亮编辑器(例如 IDEA 或 VS.NET + Resharper),您总能找到正在测试您的代码的位置。

通过 REST/HTTP 测试,您可以很好地验证您的系统是否确实有效。但是测试运行速度很慢,因此很难在这个级别进行完整的验证。我假设您的方法接受多个参数或可能的 XML 输入。要检查 XML 中的每个节点或每个参数,需要进行数十或数百次调用。你可以通过单元测试来做到这一点,但你不能通过 REST 调用来做到这一点,因为每个调用都可能需要一秒钟的时间。

我们的单元测试比#3 测试更频繁地检查特殊的边界条件。他们(#3)检查主要功能是否正常工作,仅此而已。这对我们来说似乎工作得很好。

于 2009-05-04T22:31:21.523 回答
2

正如许多人所提到的,集成测试会告诉你系统是否工作,单元测试会告诉你哪里不工作。严格从测试的角度来看,这两种测试是相辅相成的。

我看不出在单个类上添加更多单元测试会有什么帮助。这不会使重构变得更加困难,因为我们更有可能需要丢弃并重新编写测试?

不会。它将使重构变得更容易和更好,并且更清楚地看到哪些重构是适当和相关的。这就是为什么我们说 TDD 是关于设计的,而不是关于测试的。对于我来说,为一种方法编写一个测试并弄清楚如何表达该方法的结果应该是什么,以便根据被测类的其他方法提出一个非常简单的实现,这是很常见的。该实现经常会进入被测类。更简单、更可靠的实现、更清晰的边界、更小的方法:TDD - 单元测试,特别是 - 引导你朝这个方向发展,而集成测试则不然。它们都很重要,也都很有用,但它们的用途不同。

Yes, you may find yourself modifying and deleting unit tests on occasion to accommodate refactorings; that's fine, but it's not hard. And having those unit tests - and going through the experience of writing them - gives you better insight into your code, and better design.

于 2009-05-05T17:03:20.977 回答
1

您可能也对这个问题和相关答案感兴趣。在那里你可以找到我对这里已经给出的答案的补充。

于 2009-05-04T20:49:35.420 回答
1

尽管您描述的设置听起来不错,但单元测试也提供了一些重要的东西。单元测试提供了精细级别的粒度。通过松散耦合和依赖注入,您几乎可以测试每个重要案例。您可以确定这些单元是坚固的;您可以使用在集成测试期间不一定会出现的大量输入或有趣的事情来仔细检查各个方法。

例如,如果您想确定性地查看一个类将如何处理需要复杂设置的某种故障(例如,从服务器检索某些内容时出现网络异常),您可以轻松编写自己的测试双网络连接类,注入它并告诉它随时抛出异常。然后,您可以确保被测类优雅地处理异常并在有效状态下继续运行。

于 2009-05-04T21:46:38.497 回答