【问题标题】:What Makes a Good Unit Test? [closed]什么是好的单元测试? [关闭]
【发布时间】:2010-09-08 20:25:48
【问题描述】:

我敢肯定,你们中的大多数人都在编写大量自动化测试,并且在进行单元测试时也遇到了一些常见的陷阱。

我的问题是,为了避免将来出现问题,您是否遵循任何编写测试的行为规则?更具体地说:良好单元测试的属性是什么,或者您如何编写测试?

鼓励语言无关的建议。

【问题讨论】:

    标签: unit-testing language-agnostic tdd integration-testing testing-strategies


    【解决方案1】:

    让我从插入源代码开始 - Pragmatic Unit Testing in Java with JUnit(也有一个带有 C#-Nunit 的版本.. 但我有这个.. 它在很大程度上是不可知的。推荐。)

    好的测试应该是一次旅行(首字母缩略词不够粘——我有一个打印出来的书里的备忘单,我不得不拿出来确保我做对了。 )

    • 自动:调用测试以及检查 PASS/FAIL 的结果应该是自动的
    • 彻底:覆盖范围;尽管错误倾向于聚集在代码中的某些区域周围,但请确保您测试所有关键路径和场景。如果您必须了解未测试区域,请使用工具
    • 可重复:测试应该每次都产生相同的结果......每次。测试不应依赖不可控的参数。
    • 独立:非常重要。
      • 测试应该一次只测试一件事。多个断言是可以的,只要它们都在测试一个特性/行为。当测试失败时,它应该查明问题的位置。
      • 测试不应相互依赖 - 隔离。没有关于测试执行顺序的假设。通过适当地使用 setup/teardown 确保在每次测试之前“清白”
    • 专业:从长远来看,您将拥有与生产一样多的测试代码(如果不是更多的话),因此您的测试代码遵循相同的良好设计标准。精心设计的方法类,具有揭示意图的名称、无重复、具有好名称的测试等。

    • 良好的测试也运行快速。任何需要超过半秒才能运行的测试......需要进行处理。测试套件运行的时间越长……运行的频率就越低。开发人员在运行之间尝试偷偷摸摸的更改越多..如果有任何问题..找出哪个更改是罪魁祸首需要更长的时间。

    2010-08 更新:

    • 可读 :这可以被认为是专业的一部分 - 但是它不能被足够强调。一个严峻的测试是找到一个不属于你的团队的人,并要求他/她在几分钟内找出被测试的行为。测试需要像生产代码一样维护——所以即使需要更多的努力也要让它易于阅读。测试应该是对称的(遵循一种模式)和简洁的(一次测试一个行为)。使用一致的命名约定(例如 TestDox 风格)。避免因“附带细节”而使测试变得混乱。成为极简主义者。

    除此之外,其他大多数都是减少低收益工作的指南:例如“不要测试你不拥有的代码”(例如第三方 DLL)。不要去测试 getter 和 setter。密切关注成本效益比或缺陷概率。

    【讨论】:

    • 我们可能不同意 Mocks 的使用,但这是一篇非常好的单元测试最佳实践文章。
    • 我会把这个作为答案,因为我发现“A TRIP”的首字母缩写词很有用。
    • 我在很大程度上同意,但我想指出,测试您不拥有的代码是有好处的……您正在测试它是否满足您的要求。您还能如何确信升级不会破坏您的系统? (当然,这样做时请牢记成本/收益比。)
    • @Craig - 我相信您指的是(界面级别)回归测试(或某些情况下的学习者测试),它记录了您所依赖的行为。我不会为第三方代码编写“单元”测试,因为 a.供应商比我更了解该代码 b.供应商不一定要保留任何特定的实现。我不控制对该代码库的更改,也不想花时间通过升级来修复损坏的测试。所以我宁愿为我使用的行为编写一些高级回归测试(并希望在损坏时得到通知)
    • @Gishu:是的,绝对!测试只能在接口级别进行;事实上,您最多应该测试您实际使用的功能。此外,在选择编写这些测试的内容时;我发现简单直接的“单元”测试框架通常非常适合。
    【解决方案2】:
    1. 不要编写庞大的测试。正如“单元测试”中的“单元”所建议的那样,尽可能使每个测试原子孤立。如果必须,请使用模拟对象创建先决条件,而不是手动重新创建过多的典型用户环境。
    2. 不要测试明显有效的东西。 避免测试来自第三方供应商的类,尤其是那些为你编写代码的框架提供核心 API 的供应商。例如,不要测试添加项到供应商的 Hashtable 类。
    3. 考虑使用代码覆盖率工具(例如 NCover)来帮助发现您尚未测试的边缘情况。
    4. 尝试在实施之前编写测试。将测试更多地视为您的实施将遵守的规范。参照。还有行为驱动开发,这是测试驱动开发的一个更具体的分支。
    5. 保持一致。如果您只为部分代码编写测试,那么它几乎没有用处。如果您在一个团队中工作,而其他一些人或全部人不编写测试,那么它也不是很有用。让自己和其他人相信测试的重要性(以及节省时间的特性),或者不要打扰。

    【讨论】:

    • 好答案。但是,如果您不对交付中的所有内容进行单元测试,那还不错。当然它是可取的,但需要有一个平衡和实用主义。回复:让你的同事加入;有时您只需要这样做来展示价值并作为参考点。
    • 我同意。但是,从长远来看,您需要能够依赖测试,即能够假设常见的陷阱会被它们捕获。否则,好处会大大减少。
    • “如果你只为你的一些代码编写测试,它几乎没有用处。”真的是这样吗?我有 20% 的代码覆盖率的项目(关键/容易失败的领域),它们对我帮助很大,而且项目也很好。
    • 我同意斯劳的观点。即使只有几个测试,只要它们写得很好并且足够独立,它们也会有很大的帮助。
    【解决方案3】:

    这里的大多数答案似乎都是针对一般的单元测试最佳实践(何时、何地、为什么以及什么),而不是实际编写测试本身(如何)。由于这个问题在“如何”部分似乎非常具体,我想我会发布这个,取自我在公司进行的“棕色袋子”演示文稿。

    Womp 的写作测试五定律:


    1.使用长的、描述性的测试方法名称。

       - Map_DefaultConstructorShouldCreateEmptyGisMap()
       - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
       - Dog_Object_Should_Eat_Homework_Object_When_Hungry()
    

    2。在Arrange/Act/Assert style 中编写您的测试。

    • 虽然这种组织战略 已经存在了一段时间并且 叫很多东西,介绍 “AAA”的首字母缩写词最近有 是一个很好的方式来解决这个问题。 使您的所有测试与 AAA 风格使它们易于阅读和 维护。

    3。始终在您的断言中提供失败消息。

    Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element 
    processing events was raised by the XElementSerializer");
    
    • 一个简单但有益的做法,它可以在您的运行程序应用程序中清楚地显示失败的原因。如果您不提供消息,您通常会在失败输出中得到类似“预期为真,为假”之类的信息,这使得您必须实际阅读测试以找出问题所在。

    4.评论测试的原因——业务假设是什么?

      /// A layer cannot be constructed with a null gisLayer, as every function 
      /// in the Layer class assumes that a valid gisLayer is present.
      [Test]
      public void ShouldNotAllowConstructionWithANullGisLayer()
      {
      }
    
    • 这似乎很明显,但是这 实践将保护完整性 来自不知道的人的测试 了解测试背后的原因 首先。我见过很多 测试被删除或修改 非常好,只是因为 这个人不明白 假设测试是 验证。
    • 如果测试是微不足道的或方法 名称足够描述性,它 可以允许离开 评论关闭。

    5.每个测试都必须始终恢复它接触到的任何资源的状态

    • 尽可能使用模拟来避免 处理真实资源。
    • 必须在测试时进行清理 等级。测试不能有任何 依赖于执行顺序。

    【讨论】:

    • +1 因为第 1、2 和 5 点很重要。如果您已经在使用描述性测试方法名称,那么 3 和 4 对于单元测试来说似乎有点过分了,但是如果它们的范围很大(功能或验收测试),我建议您记录测试。
    【解决方案4】:

    牢记这些目标(改编自 Meszaros 的 xUnit 测试模式一书)

    • 测试应该降低风险,而不是 介绍一下。
    • 测试应该易于运行。
    • 测试应该易于维护,因为 系统围绕他们发展

    一些使这更容易的事情:

    • 测试应该只因为以下原因而失败 一个原因。
    • 测试应该只测试一件事
    • 最小化测试依赖(无 对数据库、文件、用户界面的依赖 等)

    不要忘记您也可以使用您的 xUnit 框架进行集成测试但将集成测试和单元测试分开

    【讨论】:

    • 我猜你的意思是你改编自 Gerard Meszaros 的“xUnit 测试模式”一书。 xunitpatterns.com
    • 优秀的分数。单元测试可能非常有用,但重要的是要避免陷入复杂、相互依赖的单元测试的陷阱,这会给任何改变系统的尝试带来巨大的负担。
    【解决方案5】:

    测试应该被隔离。一项测试不应依赖于另一项测试。更进一步,测试不应该依赖于外部系统。换句话说,测试您的代码,而不是您的代码所依赖的代码。您可以将这些交互作为集成或功能测试的一部分进行测试。

    【讨论】:

      【解决方案6】:

      优秀单元测试的一些属性:

      • 当测试失败时,应该立即发现问题所在。如果您必须使用调试器来跟踪问题,那么您的测试不够精细。每个测试只有一个断言会有所帮助。

      • 重构时,任何测试都不应该失败。

      • 测试应该运行得如此之快,以至于您毫不犹豫地运行它们。

      • 所有测试都应该始终通过;没有不确定的结果。

      • 单元测试应该是精心设计的,就像您的生产代码一样。

      @Alotor:如果你建议一个库应该只在其外部 API 上进行单元测试,我不同意。我想要对每个类进行单元测试,包括我不向外部调用者公开的类。 (不过,if I feel the need to write tests for private methods, then I need to refactor.


      编辑:有一条关于“每个测试一个断言”引起的重复的评论。具体来说,如果您有一些代码来设置场景,然后想要对其进行多个断言,但每个测试只有一个断言,您可能会在多个测试中重复设置。

      我不采用这种方法。相反,我使用每个场景的测试装置。这是一个粗略的例子:

      [TestFixture]
      public class StackTests
      {
          [TestFixture]
          public class EmptyTests
          {
              Stack<int> _stack;
      
              [TestSetup]
              public void TestSetup()
              {
                  _stack = new Stack<int>();
              }
      
              [TestMethod]
              [ExpectedException (typeof(Exception))]
              public void PopFails()
              {
                  _stack.Pop();
              }
      
              [TestMethod]
              public void IsEmpty()
              {
                  Assert(_stack.IsEmpty());
              }
          }
      
          [TestFixture]
          public class PushedOneTests
          {
              Stack<int> _stack;
      
              [TestSetup]
              public void TestSetup()
              {
                  _stack = new Stack<int>();
                  _stack.Push(7);
              }
      
              // Tests for one item on the stack...
          }
      }
      

      【讨论】:

      • 我不同意每个测试只有一个断言。您在测试中拥有的断言越多,您将拥有的剪切和粘贴测试用例就越少。我认为测试用例应该关注场景或代码路径,并且断言应该源于满足该场景的所有假设和要求。
      • 我认为我们同意 DRY 适用于单元测试。正如我所说,“单元测试应该考虑周全”。但是,有多种方法可以解决重复问题。一个,正如你提到的,是有一个单元测试,首先调用被测代码,然后多次断言。另一种方法是为场景创建一个新的“测试夹具”,它在初始化/设置步骤期间调用被测代码,然后进行一系列简单的单元测试。
      • 我的经验法则是,如果您使用复制粘贴,那么您做错了什么。我最喜欢的一句话是“复制粘贴不是设计模式”。我也同意每个单元测试一个断言通常是一个好主意,但我并不总是坚持。我喜欢更一般的“每个单元测试测试一件事”。虽然这通常会转化为每个单元测试一个断言。
      【解决方案7】:

      您所追求的是描述被测类的行为。

      1. 验证预期行为。
      2. 验证错误案例。
      3. 覆盖类中的所有代码路径。
      4. 执行类中的所有成员函数。

      基本意图是增加您对班级行为的信心。

      这在重构代码时特别有用。 Martin Fowler 有一个有趣的article 在他的网站上进行测试。

      HTH。

      干杯,

      罗伯

      【讨论】:

      • Rob - 机械式这很好,但它没有达到目的。你为什么做这一切?以这种方式思考可能会帮助其他人走上 TDD 的道路。
      【解决方案8】:

      测试最初应该失败。然后你应该编写使它们通过的代码,否则你会冒着编写一个有错误但总是通过的测试的风险。

      【讨论】:

      • @Rismo 本身并非排他性的。根据定义,Quarrelsome 在这里写的内容是“测试优先”方法独有的,它是 TDD 的一部分。 TDD 还考虑了重构。我读过的最“聪明的裤子”定义是 TDD = Test First + Refactor。
      • 是的,它不一定是 TDD,只要确保你的测试首先失败。然后将其余部分连接起来。这在进行 TDD 时最常发生,但您也可以在不使用 TDD 时应用它。
      【解决方案9】:

      我喜欢上述 Pragmatic Unit Testing 书中的 Right BICEP 首字母缩略词:

      • 正确:结果是否正确
      • B:所有b条件都正确吗?
      • :我们可以检查反向关系吗?
      • C:我们可以c用其他方式检查结果吗?
      • E:我们可以强制e错误情况发生吗?
      • Pp性能特征是否在界限内?

      我个人觉得你可以通过检查你得到正确的结果(1+1 应该在加法函数中返回 2),尝试所有你能想到的边界条件(例如使用两个数字和大于 add 函数中的整数最大值)并强制错误条件,例如网络故障。

      【讨论】:

        【解决方案10】:

        好的测试需要可维护。

        我还没有完全弄清楚如何在复杂的环境中做到这一点。

        随着您的代码库开始达到目标,所有教科书都开始脱胶 成数百或数百万行代码。

        • 团队互动爆炸式增长
        • 测试用例数量激增
        • 组件之间的交互爆炸。
        • 构建所有单元测试的时间成为构建时间的重要组成部分
        • API 更改可能会波及数百个测试用例。尽管生产代码更改很容易。
        • 将进程排序到正确状态所需的事件数量增加,这反过来又增加了测试执行时间。

        好的架构可以控制一些交互爆炸,但不可避免的是 随着自动化测试系统的发展,系统变得更加复杂。

        这是您开始必须权衡取舍的地方:

        • 仅测试外部 API,否则重构内部会导致大量测试用例返工。
        • 随着封装子系统保留更多状态,每个测试的设置和拆卸变得更加复杂。
        • 夜间编译和自动化测试执行时间会增加到数小时。
        • 编译和执行时间增加意味着设计人员不会或不会运行所有测试
        • 为了减少测试执行时间,您考虑对测试进行排序以减少设置和拆卸

        您还需要决定:

        您将测试用例存储在代码库的什么位置?

        • 您如何记录您的测试用例?
        • 可以重复使用测试夹具来节省测试用例维护吗?
        • 当夜间测试用例执行失败时会发生什么?谁负责分流?
        • 如何维护模拟对象?如果您有 20 个模块都使用自己风格的模拟日志 API,那么更改 API 会很快产生影响。不仅测试用例发生了变化,而且 20 个模拟对象也发生了变化。这 20 个模块是由许多不同的团队花费数年时间编写的。这是一个经典的重用问题。
        • 个人及其团队了解自动化测试的价值,他们只是不喜欢其他团队的做法。 :-)

        我可以永远继续下去,但我的意思是:

        测试需要可维护。

        【讨论】:

          【解决方案11】:

          我不久前在 This MSDN Magazine article 中介绍了这些原则,我认为这对任何开发人员都很重要。

          我定义“好”单元测试的方式是,它们是否具有以下三个属性:

          • 它们是可读的(命名、断言、变量、长度、复杂性..)
          • 它们是可维护的(没有逻辑,没有过度指定,基于状态,重构......)
          • 它们值得信赖(测试正确的东西,隔离,​​而不是集成测试..)

          【讨论】:

          • 罗伊,我完全同意。这些事情比边缘情况覆盖范围重要得多。
          【解决方案12】:
          • 单元测试只是测试单元的外部 API,您不应该测试内部行为。
          • TestCase 的每个测试都应测试此 API 中的一个(且仅一个)方法。
            • 应为失败案例添加额外的测试案例。
          • 测试测试的覆盖率:一旦测试了一个单元,该单元内 100% 的行应该已经执行。

          【讨论】:

            【解决方案13】:

            Jay Fields 有一个关于编写单元测试的lot of good advices,还有一个a post where he summarize the most important advices。在那里你会读到你应该批判性地思考你的背景并判断这些建议是否对你有价值。您会在这里获得大量惊人的答案,但由您决定哪个最适合您的上下文。尝试它们,如果你觉得不好就重构。

            亲切的问候

            【讨论】:

              【解决方案14】:

              永远不要假设一个简单的 2 行方法会起作用。编写快速单元测试是防止空测试丢失、负号放错位置和/或细微的范围界定错误困扰您的唯一方法,当您处理它的时间比现在更少时,这是不可避免的。

              【讨论】:

                【解决方案15】:

                我支持“A TRIP”的答案,除了测试应该相互依赖!!!

                为什么?

                DRY - 不要重复自己 - 也适用于测试!测试依赖项可以帮助 1) 节省设置时间,2) 节省夹具资源,以及 3) 查明故障。当然,前提是您的测试框架支持一流的依赖项。否则,我承认,它们很糟糕。

                关注http://www.iam.unibe.ch/~scg/Research/JExample/

                【讨论】:

                • 我同意你的看法。 TestNG 是另一个容易允许依赖的框架。
                【解决方案16】:

                单元测试通常基于模拟对象或模拟数据。 我喜欢写三种单元测试:

                • “瞬态”单元测试:他们创建自己的模拟对象/数据并用它测试他们的功能,但会破坏所有内容并且不留下任何痕迹(就像测试数据库中没有数据一样)
                • “持久”单元测试:他们在您的代码中测试函数,创建对象/数据,这些对象/数据稍后将被更高级的函数用于他们自己的单元测试(避免这些高级函数在每次他们自己的一组模拟对象时重新创建/数据)
                • “基于持久性”的单元测试:使用持久性单元测试已经存在的模拟对象/数据进行单元测试(因为在另一个单元测试会话中创建)。

                关键是要避免重播一切,以便能够测试每个功能。

                • 我经常运行第三种类型,因为所有模拟对象/数据都已经存在。
                • 每当我的模型发生变化时,我都会运行第二种。
                • 我偶尔会运行第一个来检查非常基本的功能,以检查基本回归。

                【讨论】:

                  【解决方案17】:

                  考虑两种类型的测试并区别对待它们 - 功能测试和性能测试。

                  为每个使用不同的输入和指标。您可能需要为每种类型的测试使用不同的软件。

                  【讨论】:

                  • 那么单元测试呢?
                  【解决方案18】:

                  我使用Roy Osherove's Unit Test Naming standards 描述的一致测试命名约定给定测试用例类中的每个方法都具有以下命名样式 MethodUnderTest_Scenario_ExpectedResult。

                    第一个测试名称部分是被测系统中方法的名称。
                    接下来是正在测试的具体场景。
                    最后是那个场景的结果。

                  每个部分都使用大写驼峰式,并由下划线分隔。

                  我发现这在我运行测试时很有用,测试按被测方法的名称分组。并且有一个约定可以让其他开发者理解测试意图。

                  如果被测方法已重载,我还会将参数附加到方法名称。

                  【讨论】:

                    猜你喜欢
                    • 2018-01-18
                    • 2011-01-22
                    • 2010-12-12
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-12-03
                    • 1970-01-01
                    • 2010-10-05
                    相关资源
                    最近更新 更多