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

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

鼓励与语言无关的建议。

有帮助吗?

解决方案

让我从插入源开始 - 使用 JUnit 在 Java 中进行实用的单元测试 (还有一个带有 C#-Nunit 的版本..但我有这个..它在很大程度上是不可知论的。受到推崇的。)

好的测试应该是 一次旅行 (缩写词不够粘——我在书中有一份备忘单的打印输出,我必须把它拿出来以确保我做对了……)

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

  • 良好的测试也可以运行 快速地. 。任何需要超过半秒才能运行的测试..需要努力。测试套件运行所需的时间越长。运行的频率越低。开发人员尝试在运行之间进行的更改越多。如果有什么坏了..需要更长的时间才能找出罪魁祸首是哪个变化。

2010 年 8 月更新:

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

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

其他提示

  1. 不要编写巨大的测试。 正如“单元测试”中的“单元”所示,将每个单元设置为 原子孤立 尽可能。如果必须,请使用模拟对象创建前提条件,而不是手动重新创建太多典型的用户环境。
  2. 不要测试明显有效的东西。 避免测试来自第三方供应商的类,尤其是提供您编码的框架的核心 API 的类。例如,不要测试将项目添加到供应商的 Hashtable 类中。
  3. 考虑使用代码覆盖率工具 例如 Ncover 来帮助发现您尚未测试的边缘情况。
  4. 尝试编写测试 实施。 将测试更多地视为您的实现将遵守的规范。比照。还有行为驱动开发,测试驱动开发的一个更具体的分支。
  5. 始终如一。 如果您只为某些代码编写测试,那么它几乎没有用。如果您在一个团队中工作,并且其他一些或所有其他人不编写测试,那么它也不是很有用。让自己和其他人相信重要性(并且 节省时间 属性)的测试,或者不用打扰。

这里的大多数答案似乎总体上涉及单元测试最佳实践(何时、何地、为什么和什么),而不是实际编写测试本身(如何)。由于这个问题似乎非常具体地涉及“如何”部分,因此我想我应该发布此内容,摘自我在公司进行的“棕色袋子”演示。

Womp 编写测试的 5 条法则:


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");
  • 这是一个简单但有益的实践,可以让您的跑步者应用程序中的失败之处变得显而易见。如果您不提供消息,您通常会在失败输出中得到类似“预期为真,为假”的信息,这使得您必须实际阅读测试以找出问题所在。

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.每个测试必须始终恢复它所触及的任何资源的状态

  • 在可能的情况下使用模拟以避免处理真实资源。
  • 清理必须在测试级别进行。测试不得依赖执行顺序。

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

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

有些事情可以让这变得更容易:

  • 测试只能因为一个原因而失败。
  • 测试应该只测试一件事
  • 最小化测试依赖项(对数据库,文件,UI等无依赖性)

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

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

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

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

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

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

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

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

@阿洛托:如果您建议库应该只在其外部 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...
    }
}

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

  1. 验证预期行为。
  2. 错误案例的验证。
  3. 覆盖类内的所有代码路径。
  4. 行使班级内的所有成员职能。

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

当考虑重构代码时,这特别有用。马丁·福勒有一个有趣的 文章 关于在他的网站上进行测试。

HTH。

干杯,

测试最初应该失败。然后,您应该编写使它们通过的代码,否则您将面临编写有错误但始终通过的测试的风险。

我喜欢前面提到的 Right BICEP 缩写 实用的单元测试 书:

  • 正确的:是否有结果 正确的?
  • :都是 基本条件正确吗?
  • :我们可以检查一下吗 逆向关系?
  • C:我们可以吗 C使用其他方式进行交叉检查结果?
  • :我们可以强迫吗 e错误情况会发生吗?
  • :是 p性能特征是否在范围内?

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

好的测试需要可维护。

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

随着您的代码库开始进入数百万或数百万的代码,所有的教科书就开始不被盖上。

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

良好的体系结构可以控制一些交互爆炸,但是随着系统变得越来越复杂,自动化测试系统随之增长。

这是您开始必须进行权衡的地方:

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

您还需要决定:

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

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

我可以永远继续下去,但我的观点是:

测试需要可维护。

我不久前介绍过这些原则 这篇 MSDN 杂志文章 我认为这对于任何开发人员来说都很重要。

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

  • 它们是可读的(命名、断言、变量、长度、复杂性......)
  • 它们是可维护的(没有逻辑,没有过度指定,基于状态,重构......)
  • 它们是值得信赖的(测试正确的事情,孤立的,而不是集成测试..)
  • 单元测试只是测试单元的外部 API,您不应该测试内部行为。
  • TestCase 的每个测试都应该测试此 API 中的一个(且仅一个)方法。
    • 对于失败案例应包括附加测试案例。
  • 测试测试的覆盖范围:一旦一个单元被测试,这个单元内的100%的行就应该被执行。

杰·菲尔兹有一个 很多好的建议 关于编写单元测试,有 他总结了最重要的建议的帖子. 。在那里你会读到,你应该批判性地思考你的背景,并判断这些建议对你是否有价值。您在这里会得到大量令人惊奇的答案,但由您决定哪个最适合您的情况。尝试一下,如果你觉得不好的话就重构。

亲切的问候

永远不要假设简单的 2 行方法会起作用。编写快速单元测试是防止缺失的空测试、错位的减号和/或微妙的范围错误困扰您的唯一方法,当您处理这些问题的时间比现在更少时,不可避免地会出现这种情况。

我赞同“一次旅行”的答案,除了 测试应该相互依赖!

为什么?

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

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

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

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

重点是避免重播 一切 为了能够测试每个功能。

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

考虑两种类型的测试并以不同的方式对待它们 - 功能测试和性能测试。

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top