什么是好的单元测试?[关闭]


97

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

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

鼓励与语言无关的建议。

Answers:


93

让我首先插入源代码- 使用JUnit用Java进行实用单元测试(也有一个带有C#-Nunit的版本..但是我有这个版本。.在很大程度上与它无关。建议。)

好的测试应该是一个旅行(首字母缩略词不够粘稠-我在书中有一份速查表的打印输出,为了确保我理解正确,我必须将其抽出。)

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

  • 好的测试也可以快速进行。任何需要半秒以上才能运行的测试都需要进行。测试套件运行的时间越长..运行频率越低。开发人员将尝试在两次运行之间进行更多的更改..如果有任何中断..将花更长的时间来找出哪个更改是罪魁祸首。

更新2010-08:

  • 可读:可以认为这是Professional的一部分-但是压力不够大。严峻的考验是找到不属于您团队的人,并要求他/她在几分钟之内找出被测行为。测试需要像生产代码一样进行维护-因此即使花费更多的精力也可以使其易于阅读。测试应该对称(遵循模式)并且简洁(一次测试一个行为)。使用一致的命名约定(例如TestDox样式)。避免使“偶然的细节”使测试混乱。

除了这些以外,其他大多数都是减少低收益工作的准则:例如“不要测试您不拥有的代码”(例如第三方DLL)。不要去测试吸气剂和吸气剂。留意成本效益比或缺陷概率。


我们可能对Mocks的使用持不同意见,但这是单元测试最佳实践的很好的表述。
贾斯汀标准时间

然后我将其作为答案,因为我发现“ A TRIP”的首字母缩写很有用。
Spoike

3
我大部分时间都同意,但要指出的是,测试您不拥有的代码会有好处……您正在测试它是否符合您的要求。您还能如何确定升级不会破坏您的系统?(但当然,这样做时请务必牢记成本/收益比率。)
破灭

@Craig-我相信您是指(接口级别)回归测试(在某些情况下为学习者测试),该文件记录了您所依赖的行为。我不会为第三方代码编写“单元”测试,因为 供应商比我更了解该代码b。供应商不保留任何特定的实现。我无法控制对该代码库的更改,也不想花费我的时间通过升级来修复损坏的测试。因此,我宁愿为自己使用的行为编写一些高级的回归测试(并希望在发生故障时得到通知)
Gishu 2010年

@Gishu:是的,绝对!测试只能在接口级别进行;实际上,您最多应该测试您实际使用的功能。此外,在选择编写这些测试的内容时;我发现简单,直接的“单元”测试框架通常非常适合。
幻灭了

42
  1. 不要编写繁琐的测试。正如“单元测试”中的“单元”所暗示的那样,使每个单元尽可能原子隔离。如果需要,请使用模拟对象创建前提条件,而不是手动重新创建太多的典型用户环境。
  2. 不要测试明显起作用的东西。避免测试第三方供应商的类,尤其是那些提供您所编码框架的核心API的类。例如,请勿测试将项目添加到供应商的Hashtable类中。
  3. 考虑使用诸如NCover之类的代码覆盖工具来帮助发现尚未测试的极端情况。
  4. 尝试在实施之前编写测试。将测试视为您的实现将遵循的更多规范。cf. 也是行为驱动开发,这是测试驱动开发的一个更具体的分支。
  5. 始终如一。如果只为某些代码编写测试,则几乎没有用。如果您在团队中工作,而其他一些或所有其他人都不编写测试,那么它也不是很有用。让自己和其他所有人相信测试的重要性(以及节省时间的属性),或者不要打扰。

1
好答案。但是,如果您不对交付中的所有内容进行单元测试,那还不错。当然,这是可取的,但需要保持平衡和务实。回复:让您的同事加入;有时您只需要这样做就可以证明价值并作为参考。
马丁·克拉克

1
我同意。但是,从长远来看,您需要能够依靠在那里的测试,即能够假定它们会捕获常见的陷阱。否则,收益将大大减少。
索伦Kuklau

2
“如果只为某些代码编写测试,则几乎没有用。” 真的是这样吗?我有一些项目的代码覆盖率达到20%(关键区域/容易出现故障的区域),它们为我提供了巨大帮助,项目也很好。
博士 邪恶的

1
我同意斯劳。即使只有很少的测试,只要编写得很好并且足够独立,它们就会为您提供极大的帮助。
Spoike

41

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

沃普的五项写作定律:


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

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

2.以“ 编排/动作/断言”风格编写测试。

  • 尽管这种组织策略已经存在了一段时间,并引起了许多关注,但最近引入“ AAA”首字母缩略词已经成为解决这一问题的好方法。使您的所有测试与AAA风格保持一致,使它们易于阅读和维护。

3.始终向您的断言提供失败消息。

Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element 
processing events was raised by the XElementSerializer");
  • 一种简单而有益的做法,使您的运行者应用程序可以清楚地发现失败了。如果不提供消息,则故障输出中通常会出现“ Expected true,was false”之类的信息,这使得您必须实际阅读测试以找出问题所在。

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.每个测试都必须始终还原其接触的任何资源的状态

  • 尽可能使用模拟以避免处理实际资源。
  • 清理必须在测试级别完成。测试不得依赖于执行顺序。

2
由于点1、2和5而+1很重要。如果您已经在使用描述性测试方法名称,则对于单元测试而言,第3和第4项似乎相当过多,但是如果它们涉及的范围较大(功能测试或验收测试),我还是建议您进行测试的文档。
Spoike

+1扎实的实践知识和范例
Phil

17

牢记这些目标(改编自Meszaros的xUnit Test Patterns)

  • 测试应该降低风险,而不是引入风险。
  • 测试应该易于运行。
  • 随着系统围绕测试的发展,测试应该易于维护

一些使这更容易的事情:

  • 测试应该仅由于一种原因而失败。
  • 测试只能测试一件事
  • 最小化测试依赖性(不依赖数据库,文件,UI等)

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


我想您的意思是您已经改编自Gerard Meszaros的“ xUnit测试模式”一书。xunitpatterns.com
Spoike

是的,你是对的。我会在帖子中
澄清一下-Mendelt

优点。单元测试可能非常有用,但是避免陷入复杂,相互依赖的单元测试的陷阱非常重要,因为单元测试会为更改系统的任何尝试带来巨大的负担。

9

测试应隔离。一种测试不应依赖于另一种。更进一步,测试不应依赖外部系统。换句话说,请测试您的代码,而不是代码所依赖的代码。您可以在集成或功能测试中测试这些交互。


9

出色的单元测试的一些属性:

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

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

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

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

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

@Alotor:如果您建议一个库仅在其外部API上进行单元测试,那么我不同意。我想要每个类的单元测试,包括我不公开给外部调用者的类。(但是,如果我觉得需要为私有方法编写测试,则需要重构。


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

我不采取这种方法。取而代之的是,我针对每种情况使用测试装置。这是一个粗糙的例子:

[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...
    }
}

我不同意每个测试只有一个断言。测试中的断言越多,剪切和粘贴的测试用例就越少。我认为测试用例应集中在方案或代码路径上,并且断言应源于实现该方案的所有假设和要求。
卢卡斯B

我认为我们同意DRY适用于单元测试。正如我所说的,“单元测试应该精心设计”。但是,有多种方法可以解决重复问题。正如您所提到的,要进行单元测试,首先调用被测代码,然后断言多次。另一种方法是为该场景创建一个新的“测试装置”,它在“初始化/设置”步骤期间调用被测代码,然后进行一系列简单的断言单元测试。
杰伊·巴祖兹

我的经验法则是,如果您使用的是复制粘贴,那么您做错了什么。我最喜欢的一句话是“复制粘贴不是设计模式”。我也同意每个单元测试一个断言通常是一个好主意,但我并不总是坚持这样做。我喜欢更通用的“每个单元测试测试一件事”。尽管通常每个单元测试都会转化为一个断言。
乔恩·特纳

7

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

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

基本目的是增强您对课堂行为的信心。

这在查看重构代码时特别有用。Martin Fowler 在他的网站上有一篇有趣的文章,关于测试。

HTH。

干杯,


Rob-机械的,这很好,但是没有达到目的。你为什么要做这一切?以这种方式思考可能会帮助其他人走上TDD的道路。
马克·李维森

7

测试最初应该失败。然后,您应该编写使它们通过的代码,否则您将冒编写有漏洞且始终通过的测试的风险。


@Rismo本身不是专有的。根据定义,Quarrelsome在此编写的内容是“测试优先”方法的专有技术,该方法是TDD的一部分。TDD还考虑了重构。我读过的最“聪明的裤子”的定义是TDD =测试优先+重构。
Spoike

是的,不必一定是TDD,只需确保测试首先失败即可。然后再连接其余部分。这在执行TDD时最常见,但是在不使用TDD时也可以应用。
Quibblesome

6

我喜欢上述实用单元测试书中的Right BICEP缩写:

  • :结果吗?
  • :是不是所有的b oundary条件是否合适?
  • :我们可以检查一下我的反向关系吗?
  • Ç:我们能否Ç罗斯检查结果用其他的方法?
  • Ë:我们可以强制Ë RROR情况发生呢?
  • P:是p范围内erformance特点?

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


6

好的测试需要维护。

我还没有弄清楚如何在复杂的环境中执行此操作。

当您的代码库开始进入成千上万或几百万行代码时,所有教科书开始脱胶。

  • 团队互动激增
  • 测试用例数量激增
  • 组件之间的相互作用会爆炸。
  • 建立所有单元测试的时间成为构建时间的重要部分
  • API更改可能会影响到数百个测试用例。即使生产代码更改很容易。
  • 将进程排序为正确状态所需的事件数量增加,从而增加了测试执行时间。

好的架构可以控制某些交互爆炸,但随着系统变得越来越复杂,不可避免的是自动化测试系统也会随之增长。

这是您必须权衡的地方:

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

您还需要确定:

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

  • 您如何记录测试用例?
  • 测试夹具可以重复使用以节省测试用例维护吗?
  • 夜间测试用例执行失败时会发生什么?谁进行分类?
  • 您如何维护模拟对象?如果您有20个模块都使用它们自己的模拟日志记录API风格,则快速更改API会产生波动。测试用例不仅会更改,而且20个模拟对象也会更改。这20个模块是由许多不同的团队在几年内编写的。这是一个经典的重用问题。
  • 个人和他们的团队了解自动化测试的价值,他们只是不喜欢另一个团队的工作方式。:-)

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

测试必须是可维护的。


5

我在《MSDN杂志》的这篇文章中提到了这些原则,我认为这对于任何开发人员来说都是重要的。

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

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

罗伊,我全心全意。这些事情比边缘案例的覆盖范围重要得多。
马特·欣兹

值得信赖-很棒!
ratkok 2011年

4
  • 单元测试仅测试单元的外部API,而不应该测试内部行为。
  • TestCase的每个测试都应在此API中测试一个(并且只有一个)方法。
    • 失败案例应包括附加测试案例。
  • 测试您的测试范围:测试完一个单元后,应该已经执行了该单元内部100%的行。


1

永远不要认为琐碎的2行方法会起作用。编写快速的单元测试是防止丢失的空测试,放错了负号和/或细微的范围错误的唯一方法,这可能会在您处理时间比现在少的时候不可避免。


1

我第二次回答“ A TRIP”,只是测试应该相互依赖!!!

为什么?

干-不要重复自己-也适用于测试!测试依存关系可以帮助1)节省设置时间,2)节省夹具资源,3)查明故障。当然,仅考虑到您的测试框架支持一流的依赖关系。否则,我承认,它们是不好的。

跟进http://www.iam.unibe.ch/~scg/Research/JExample/


我同意你的意见。TestNG是另一个可以轻松允许依赖关系的框架。
Davide

0

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

  • “瞬态”单元测试:它们创建自己的模拟对象/数据并使用其测试其功能,但是销毁所有内容并且不留下任何痕迹(例如测试数据库中没有数据)
  • “持久”单元测试:它们在代码中测试创建对象/数据的功能,这些功能稍后将由更高级的功能用于其自身的单元测试(避免那些高级功能每次在其自己的一组模拟对象/数据中都重新创建)
  • “基于持久”的单元测试:使用持久单元测试已经存在的模拟对象/数据(由于在另一个单元测试会话中创建)进行单元测试。

关键是要避免重播所有内容以便能够测试每个功能。

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

0

考虑一下这两种测试,并分别对待它们-功能测试和性能测试。

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


那单元测试呢?
Spoike

0

我使用Roy Osherove的单元测试命名标准描述的一致的测试命名约定。给定测试用例类中的每个方法都具有以下命名方式MethodUnderTest_Scenario_ExpectedResult。

    第一个测试名称部分是被测系统中方法的名称。
    接下来是正在测试的特定方案。
    最后是该方案的结果。

每个部分均使用上驼峰式保护套,并由得分下限分隔。

在运行测试时,我发现这很有用,因为测试按测试方法的名称分组。并且有一个约定允许其他开发人员了解测试意图。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.