97

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

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

鼓励语言无关的建议。

4

18 回答 18

93

让我从插入源代码开始 -使用 JUnit 在 Java 中进行实用单元测试(也有一个带有 C#-Nunit 的版本.. 但我有这个.. 它在大多数情况下是不可知的。推荐。)

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

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

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

2010-08 更新:

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

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

于 2008-09-15T04:50:59.003 回答
42
  1. 不要编写庞大的测试。正如“单元测试”中的“单元”所暗示的那样,使每一个都尽可能原子孤立。如果必须,请使用模拟对象创建先决条件,而不是手动重新创建过多的典型用户环境。
  2. 不要测试明显有效的东西。避免测试来自第三方供应商的类,尤其是提供您编码的框架的核心 API 的类。例如,不要测试将项目添加到供应商的 Hashtable 类。
  3. 考虑使用诸如 NCover 之类的代码覆盖率工具来帮助发现您尚未测试的边缘情况。
  4. 尝试在实施之前编写测试。将测试更多地视为您的实现将遵守的规范。参照。还有行为驱动开发,测试驱动开发的一个更具体的分支。
  5. 始终如一。如果您只为某些代码编写测试,那么它几乎没有用处。如果您在一个团队中工作,而其他一些人或全部人不编写测试,那么它也不是很有用。让自己和其他人相信测试的重要性(以及节省时间的特性),或者不要打扰。
于 2008-09-14T15:36:26.730 回答
41

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

Womp 的 5 条写作测试法则:


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

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

2. 以Arrange/Act/Assert 风格编写测试。

  • 虽然这种组织战略已经存在了一段时间并且有很多东西,但最近引入“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. 每个测试都必须总是恢复它接触到的任何资源的状态

  • 尽可能使用模拟来避免处理真实资源。
  • 清理必须在测试级别完成。测试不得依赖于执行顺序。
于 2009-05-06T20:18:37.007 回答
17

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

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

一些使这更容易的事情:

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

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

于 2008-09-14T15:31:22.937 回答
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...
    }
}
于 2008-09-14T16:22:22.703 回答
9

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

于 2008-09-14T16:51:14.133 回答
7

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

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

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

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

HTH。

干杯,

于 2008-09-14T15:37:52.857 回答
7

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

于 2008-09-14T15:38:02.327 回答
6

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

  • 对:结果
  • B:所有的边界条件都正确吗?
  • :我们可以检查反向关系吗?
  • C : 我们可以用其他方式交叉检查结果吗
  • E : 我们可以强制错误条件发生吗?
  • P:性能特征是否在界限内?

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

于 2008-09-15T11:23:57.903 回答
6

好的测试需要可维护。

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

当您的代码库开始达到数百或数百万行代码时,所有教科书都开始脱节。

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

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

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

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

您还需要决定:

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

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

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

测试需要可维护。

于 2009-04-24T18:46:27.967 回答
5

不久前,我在这篇 MSDN 杂志文章中介绍了这些原则,我认为这对任何开发人员都很重要。

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

  • 它们是可读的(命名、断言、变量、长度、复杂性..)
  • 它们是可维护的(没有逻辑,没有过度指定,基于状态,重构......)
  • 他们是值得信赖的(测试正确的东西,隔离,​​而不是集成测试..)
于 2008-09-28T19:35:18.313 回答
4
  • 单元测试只是测试你单元的外部 API,你不应该测试内部行为。
  • TestCase 的每个测试都应该测试这个 API 中的一个(并且只有一个)方法。
    • 对于失败案例,应该包括额外的测试案例。
  • 测试你的测试覆盖率:一旦一个单元被测试,这个单元内的 100% 的行应该已经被执行了。
于 2008-09-14T15:40:17.250 回答
2

Jay Fields 有很多关于编写单元测试的好建议,并且有一篇文章他总结了最重要的建议。在那里你会读到你应该批判性地思考你的背景并判断这些建议是否对你有价值。您会在这里获得大量惊人的答案,但由您决定哪个最适合您的上下文。试试它们,如果你觉得不好,就重构。

亲切的问候

于 2008-09-15T03:52:27.350 回答
1

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

于 2008-09-14T15:52:44.787 回答
1

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

为什么?

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

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

于 2008-10-07T21:44:19.283 回答
0

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

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

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

  • 我经常运行第三种,因为所有模拟对象/数据都已经存在。
  • 每当我的模型发生变化时,我都会运行第二种。
  • 我运行第一个来检查非常基本的功能,检查基本的回归。
于 2008-09-14T21:04:14.133 回答
0

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

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

于 2008-09-14T21:34:54.667 回答
0

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

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

每个部分使用 Upper Camel Case 并由下划线分隔。

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

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

于 2010-08-14T17:41:52.867 回答