【问题标题】:Unit test adoption [closed]单元测试采用[关闭]
【发布时间】:2009-05-28 14:00:20
【问题描述】:

我们已经尝试在当前项目中引入单元测试,但它似乎不起作用。额外的代码似乎已经成为维护方面的难题,因为当我们的内部框架发生变化时,我们必须四处走动并修复任何挂起它的单元测试。

我们有一个抽象基类用于对我们的控制器进行单元测试,它充当调用子类抽象方法实现的模板,即框架调用 Initialize,因此我们的控制器类都有自己的 Initialize 方法。

我曾经是单元测试的倡导者,但它似乎不适用于我们当前的项目。

谁能帮助确定问题以及我们如何使单元测试为我们工作而不是对我们不利?

【问题讨论】:

  • 有趣的问题。请随时通知我们!

标签: unit-testing tdd automated-tests agile


【解决方案1】:

提示:

避免编写程序代码

如果测试是针对严重依赖全局状态或位于丑陋方法主体中的过程式代码编写的,则测试可能难以维护。 如果您使用 OO 语言编写代码,use OO constructs 可以有效地减少这种情况。

  • 尽可能避免使用全局状态。
  • 避免静态,因为它们往往会波及您的代码库并最终导致不应该是静态的事物。它们还会使您的测试上下文膨胀(见下文)。
  • 有效利用多态性防止excessive ifs and flags

找出变化的地方,将其封装并将其与保持不变的地方分开。

代码中的阻塞点比其他部分更频繁地更改。在您的代码库中执行此操作,您的测试将变得更加健康。

  • 良好的封装会带来良好的松散耦合设计。
  • 重构和模块化。
  • 让测试保持小而专注。

围绕测试的上下文越大,维护起来就越困难。

尽你所能缩小测试和执行它们的环境。

  • 使用组合方法重构来测试更小的代码块。
  • 您使用的是较新的测试框架,例如 TestNG 或 JUnit4? 它们允许您通过在测试生命周期中提供更细粒度的挂钩来消除测试中的重复。
  • 调查使用测试替身(模拟、伪造、存根)以减小测试上下文的大小。
  • 调查Test Data Builder 模式。

从测试中删除重复,但确保它们保持焦点。

您可能无法删除所有重复项,但仍尝试将其删除导致疼痛的地方。确保你没有删除太多的重复,以至于有人无法一目了然地告诉测试做了什么。 (有关同一概念的另一种解释,请参阅 Paul Wheaton 的 "Evil Unit Tests" 文章。)

  • 如果无法弄清楚它在做什么,没有人会想要修复测试。
  • 遵循 Arrange、Act、Assert 模式。
  • 每个测试只使用一个断言。

在正确的级别上测试您要验证的内容。

想想记录和回放 Selenium 测试所涉及的复杂性,以及与测试单一方法相比,你会发生什么变化。

  • 相互隔离依赖关系。
  • 使用依赖注入/控制反转。
  • 使用测试替身来初始化对象以进行测试,并确保您正在单独测试单个代码单元。
  • 确保您正在编写相关测试
    • “跳出陷阱”通过故意引入错误并确保它被测试捕获。
  • 另见:Integration Tests Are A Scam

了解何时使用基于状态和基于交互的测试

真正的单元测试需要真正的隔离。单元测试不会命中数据库或打开套接字。停止嘲笑这些交互。验证您与协作者的对话是否正确,而不是此方法调用的正确结果是“42”。

演示试驾代码

一个特定的团队是否会测试驱动所有代码,或者是否会为每一行代码编写“测试优先”,这是有待商榷的。但是他们应该至少先写一些测试吗?绝对地。在某些情况下,测试优先无疑是解决问题的最佳方式。

资源:

【讨论】:

  • 为什么这个问题没有得到更多的支持?这是一些好东西!
  • 这是我的一个存根博客条目...我可能会重申这个问题并尽快发布。
  • 说真的,每个测试一个断言?你花了多少时间编写测试方法签名?
  • 另外,你为什么不喜欢静态方法?我经常发现如果我有一堆静态方法(当然,没有副作用)然后有变异实例方法的形式是“mem = f1(mem1,mem2);mem2 = f2(mem1) ;",然后只测试静态方法。
  • @erikkallen - w/r/t 静态方法,静态方法是一个黑盒子。出于测试的目的,我无法更改任何内容。他们倾向于传播全局状态的使用,因此使测试更加困难。如果您编写一个不使用全局状态的静态方法,则该方法中使用的所有状态都可能通过参数进入。为什么不把它写成实例方法呢?请参阅:googletesting.blogspot.com/2008/12/…,这不是宇宙法则,但如果您关心可测试性或可维护测试,这是一个好主意。
【解决方案2】:

您是否在测试足够小的代码单元?除非您从根本上更改核心代码中的所有内容,否则您不应该看到太多更改。

一旦事情稳定下来,您会更加欣赏单元测试,但即使是现在,您的测试也突出了您的框架更改的传播程度。

这是值得的,尽你所能坚持下去。

【讨论】:

  • 我同意 ck 的观点,因为我在几家公司中看到了单元测试集成,单元​​测试框架更常用于功能测试而不是单元测试。
  • 几个月前我写了一篇关于这个主题的博客:cwash.org/2009/02/17/dont-unit-test-anymore-no-really
  • 也就是说,“开发人员测试”术语如何与“单元测试”术语混淆以及混淆原因的含义......
【解决方案3】:

如果没有更多信息,很难对您遇到这些问题的原因做出正确的解释。有时,更改接口等不可避免地会破坏很多东西,有时则归结为设计问题。

尝试对您看到的故障进行分类是个好主意。你有什么样的问题?例如。是由于 API 更改导致的测试维护(如在重构后编译它们!),还是归结为 API 更改的行为?如果你能看到一个模式,那么你可以尝试改变生产代码的设计,或者更好地隔离测试的变化。

如果更改少量内容会在很多地方对您的测试套件造成无法估量的破坏,那么您可以做一些事情(其中大部分只是常见的单元测试技巧):

  • 开发小代码单元并进行测试 小的代码单元。提炼 它所在的接口或基类 有意义,因此代码单元 其中有“接缝”。越多 您必须引入的依赖项(或 更糟糕的是,在类中实例化 使用“新”),更多地暴露于 更改您的代码将。如果每个 代码单元有几个 依赖项(有时是一对或 根本没有)那更好 不受变化影响。

  • 只断言测试的内容 需要。不要断言中间, 偶然或无关的状态。由...设计 合同并按合同进行测试(例如 如果您正在测试堆栈弹出方法, 之后不要测试 count 属性 push——应该在一个 单独测试)。

    我看到了这个问题 相当多,尤其是如果每次测试 是一个变种。如果其中任何一个 偶然的状态变化,它打破 一切断言它 (是否需要断言或 不是)。

  • 就像使用普通代码一样,使用工厂和构建器 在你的单元测试中。我在大约 40 次测试时学会了那个 需要在 API 更改后更新构造函数调用...

  • 同样重要的是,使用前面 先上门。你的测试应该总是 如果可用,请使用正常状态。仅在必要时使用基于交互的测试(即没有要验证的状态)。

无论如何,这个要点是我会尝试找出测试失败的原因/位置,然后从那里开始。尽力使自己免受变化的影响。

【讨论】:

    【解决方案4】:

    单元测试的一个好处是,当您进行这样的更改时,您可以证明您没有破坏您的代码。您确实必须使您的测试与您的框架保持同步,但这项相当平凡的工作比试图找出重构时发生的问题要容易得多。

    【讨论】:

      【解决方案5】:

      我会坚持让您坚持 TDD。 尝试检查您的单元测试框架,与您的团队一起进行一次 RCA(根本原因分析)并确定该区域。

      在套件级别修复单元测试代码,不要频繁更改代码,特别是函数名或其他模块。

      如果您能很好地分享您的案例研究,我们将不胜感激,然后我们可以在问题领域进行更多挖掘?

      【讨论】:

        【解决方案6】:

        好问题!

        设计好的单元测试就像设计软件本身一样困难。开发人员很少承认这一点,因此结果通常是仓促编写的单元测试,每当被测系统发生变化时就需要维护。因此,您的问题的部分解决方案可能是花费更多时间来改进单元测试的设计。

        我可以推荐一本值得称其为 The Design Patterns of Unit-Testing 的好书

        HTH

        【讨论】:

        • 绝对是一本好书,可以解决提问者的问题
        • 很多书籍可以帮助您开始使用 TDD 和单元测试。当你遇到困难时,上面的书可以提供帮助。
        • 确实如此。如果卡住了,只需要在正确的页面打开即可!
        【解决方案7】:

        如果问题是您的测试与实际代码过时,您可以执行以下一项或两项操作:

        1. 培训所有开发人员不要通过不更新单元测试的代码审查。
        2. 设置一个自动测试框,在每次签入后运行全套单元测试,并向破坏构建的人发送电子邮件。 (我们曾经认为这只是为“大男孩”准备的,但我们在专用盒子上使用了开源包。)

        【讨论】:

          【解决方案8】:

          如果代码中的逻辑发生了变化,并且您已经为这些代码段编写了测试,我认为需要更改测试以检查新逻辑。单元测试应该是测试代码逻辑的相当简单的代码。

          【讨论】:

            【解决方案9】:

            您的单元测试正在做他们应该做的事情。由于框架、即时代码或其他外部资源的变化而导致的任何行为中断都浮出水面。这应该做的是帮助您确定行为是否确实发生了变化并且单元测试是否需要相应地修改,或者是否引入了错误从而导致单元测试失败并需要更正。

            不要放弃,虽然现在很沮丧,但好处会实现的。

            【讨论】:

              【解决方案10】:

              我不确定导致难以维护代码测试的具体问题,但是当我在测试中断时遇到类似问题时,我可以分享一些我自己的经验。我最终了解到,缺乏可测试性主要是由于被测类的一些设计问题:

              • 使用具体类而不是接口
              • 使用单例
              • 为业务逻辑和数据访问调用大量静态方法,而不是接口方法

              因此,我发现我的测试通常会中断——不是因为被测类发生了变化——而是因为被测类调用的其他类发生了变化。一般来说,重构类以询问它们的数据依赖关系并使用模拟对象进行测试(Java 的 EasyMock 等人)使测试更加集中和可维护。我真的很喜欢一些关于这个主题的网站:

              【讨论】:

              【解决方案11】:

              为什么每次对框架进行更改时都必须更改单元测试? 不应该反过来吗?

              如果您使用的是 TDD,那么您应该首先确定您的测试正在测试错误的行为,并且应该验证所需的行为是否存在。现在您已经修复了测试,但您的测试失败了,您必须消除框架中的错误,直到您的测试再次通过。

              【讨论】:

                【解决方案12】:

                当然,一切都伴随着价格。在开发的早期阶段,必须更改大量单元测试是正常的。

                您可能需要查看代码的某些部分以进行更多封装、创建更少的依赖项等。

                当你接近生产日期时,你会很高兴你有这些测试,相信我:)

                【讨论】:

                • 我们现在正处于发布周期中,这就是问题所在,因为当我们需要修复站点验收错误时,我们无法花时间修复测试。
                • @Burt 这没有任何意义。您首先为您不想想要更改的行为编写单元测试。如果您希望行为每隔一天改变一次,那么不要编写单元测试。有点奇怪的是写了它们然后抱怨它们失败了。只有当您想要具有稳定、可重复行为的代码时,您才需要单元测试。如果您希望您的软件不断改变它的功能,那么不要对它进行单元测试。不过,在这种情况下,祝您手动测试好运。
                • 我们如何在不改变框架的情况下发展框架?它是基础架构代码,我使用城堡堆栈处理了一段时间的主干代码,它发生了很大变化,所以我认为框架通过重构和在识别新需求时进行更改是很常见的。
                • 要么 (A) 你自动测试它,然后当行为改变时你必须修改测试; (B) 你手动测试它,使用 Word 文档或对行为的口碑预期,然后当你测试它时,你必须告诉测试人员“嘿乔,从现在开始,当你做 X 和 Y 时,Z 必须发生,而不是 W",或者 (C) 你没有测试它。对于某些中间层,您似乎介于 (A) 和 (C) 之间。两者都有成本和优势。
                【解决方案13】:

                你的单元测试是不是太面向黑盒了?我的意思是......让我举个例子:假设您正在对某种容器进行单元测试,您是否使用容器的 get() 方法来验证新项目是否实际存储,或者您是否设法获得一个句柄实际存储以直接检索项目的存储位置?后者使测试变得脆弱:当您更改实现时,您正在破坏测试。

                您应该测试接口,而不是内部实现。

                当您更改框架时,您最好先尝试更改测试,然后再更改框架。

                【讨论】:

                  【解决方案14】:

                  我建议投资于测试自动化工具。如果您正在使用持续集成,则可以使其协同工作。那里有一些工具可以扫描您的代码库并为您生成测试。然后将运行它们。这种方法的缺点是它太通用了。因为在很多情况下,单元测试的目的是破坏系统。 我已经编写了许多测试,是的,如果代码库发生更改,我必须更改它们。

                  自动化工具有一条很好的路线,您肯定会拥有更好的代码覆盖率。

                  但是,通过基于开发人员的良好测试,您也可以测试系统完整性。

                  希望这会有所帮助。

                  【讨论】:

                    【解决方案15】:

                    如果您的代码真的很难测试并且测试代码中断或需要花费很多精力来保持同步,那么您的问题就更大了。

                    考虑使用 extract-method 重构来抽出一小块只做一件事且只做一件事的代码块;没有依赖关系并将您的测试写入那些小方法。

                    【讨论】:

                      【解决方案16】:

                      额外的代码似乎已经成为维护方面的难题,因为当我们的内部框架发生变化时,我们必须四处走动并修复任何挂起它的单元测试。

                      另一种方法是,当您的框架发生更改时,您不测试更改。或者你根本不测试框架。那是你要的吗?

                      您可以尝试重构您的框架,使其由可以独立测试的较小部分组成。然后,当您的框架发生更改时,您希望 (a) 更改更少的部分或 (b) 更改主要是在组成部分的方式上。无论哪种方式都可以让您更好地重用代码和测试。但是涉及到真正的智力努力;不要指望这很容易。

                      【讨论】:

                      • 理想情况下,我们的框架应该是接口驱动的,这意味着我们可以模拟其中的很多内容,但我们认为继承是更好的选择,正是继承导致了问题并使功能难以实现测试。
                      【解决方案17】:

                      我发现,除非您使用鼓励编写非常小的类的 IoC / DI 方法,并严格遵循单一职责原则,否则单元测试最终会测试多个类的交互,这使得它们非常复杂,因此很脆弱。

                      我的观点是,许多新颖的软件开发技术只有在一起使用时才能发挥作用。特别是 MVC、ORM、IoC、单元测试和模拟。 DDD(在现代原始意义上)和 TDD/BDD 更加独立,因此您可以使用或不使用它们。

                      【讨论】:

                        【解决方案18】:

                        有时设计 TDD 测试会对应用程序本身的设计提出质疑。检查你的类是否设计得很好,你的方法一次只执行一件事......有了好的设计,编写代码来测试简单的方法和类应该很简单。

                        【讨论】:

                          【解决方案19】:

                          我自己一直在思考这个话题。我非常看重单元测试的价值,但不是严格的 TDD。在我看来,在某种程度上,您可能正在进行探索性编程,其中将事物划分为类/接口的方式需要改变。如果您在旧类结构的单元测试上投入了大量时间,那会增加对重构的惯性,丢弃额外的代码会很痛苦,等等。

                          【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 2010-09-08
                            • 2011-11-07
                            • 2010-12-21
                            • 2017-12-16
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 2018-01-18
                            相关资源
                            最近更新 更多