什么是良好的单元测试?

时间:2008-09-14 15:20:45

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

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

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

鼓励语言不可知的建议。

18 个答案:

答案 0 :(得分:93)

让我先来看一下插件来源 - Pragmatic Unit Testing in Java with JUnit(还有一个带有C#-Nunit的版本......但是我有这个...它的大部分都是不可知的。推荐。)

好的测试应该一个TRIP (这个首字母缩略词不够粘 - 我在书中打印了一张cheatsheet,我必须拔出来确保我做对了.. )

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

  • 好的测试也可以快速。任何需要超过半秒才能运行的测试......需要加以研究。测试套件运行所需的时间越长,运行的频率就越低。开发人员试图在两次运行之间潜行的变化越多......如果有什么破坏......需要更长的时间来确定哪个变化是罪魁祸首。

2010-08更新:

  • 可读:这可以被认为是专业人士的一部分 - 但是不能强调它。酸测试是找到一个不属于你的团队的人,并要求他/她在几分钟内弄清楚被测试的行为。测试需要像生产代码一样进行维护 - 因此即使需要付出更多努力也可以轻松阅读。测试应该是对称的(遵循模式)和简洁(一次测试一个行为)。使用一致的命名约定(例如TestDox样式)。避免使用“附带细节”使测试变得混乱..变得极简。

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

答案 1 :(得分:42)

  1. 不要编写巨大的测试。正如“单元测试”中的“单位”所示,将每个单元设为 atomic 隔离尽可能。如果必须,请使用模拟对象创建前置条件,而不是手动重新创建过多的典型用户环境。
  2. 不要测试明显有用的东西。避免测试来自第三方供应商的类,尤其是提供您编码的框架的核心API的类。例如,不要测试将项添加到供应商的Hashtable类。
  3. 考虑使用代码覆盖率工具(例如NCover)来帮助发现尚未测试的边缘情况。
  4. 尝试在实施之前编写测试将测试视为您的实施将遵循的更多规范。参看也是行为驱动的开发,是测试驱动开发的一个更具体的分支。
  5. 保持一致。如果您只为某些代码编写测试,那么它几乎没用。如果你在一个团队中工作,而其他一些或所有人都没有编写测试,那么它也不是很有用。说服自己和其他所有人了解测试的重要性(以及节省时间的属性),或者不要打扰。

答案 2 :(得分:41)

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

Womp的5个写作测试法则:


<强> 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。每次测试都必须始终恢复其接触的任何资源的状态

  • 尽可能避免使用模拟 处理实际资源。
  • 必须在测试时进行清理 水平。测试不得有任何 依赖执行令。

答案 3 :(得分:17)

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

  • 测试应该降低风险,而不是 介绍它。
  • 测试应该很容易运行。
  • 测试应该易于维护 系统围绕着他们发展

使这更容易的一些事情:

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

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

答案 4 :(得分:9)

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

  • 当测试失败时,应立即明白问题所在。如果必须使用调试器来追踪问题,那么您的测试就不够精确。每次测试只有一个断言有帮助。

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

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

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

  • 单元测试应该是一个很好的因素,就像您的生产代码一样。

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

答案 5 :(得分:9)

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

答案 6 :(得分:7)

你所追求的是被测试阶级行为的界定。

  1. 预期行为的验证。
  2. 验证错误案例。
  3. 覆盖班级内所有代码路径。
  4. 在课堂上练习所有成员函数。
  5. 基本意图是增加你对班级行为的信心。

    在查看重构代码时,这非常有用。 Martin Fowler在他的网站上有一个关于测试的有趣article

    HTH。

    欢呼声,

    罗布

答案 7 :(得分:7)

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

答案 8 :(得分:6)

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

  • 正确:结果正确
  • B :所有 b 条件都是正确的吗?
  • :我们可以查看正面关系吗?
  • C :我们可以 c 使用其他方式检查结果吗?
  • E :我们可以强制执行 e 错误情况吗?
  • P :界限范围内的 p 性能特征是什么?

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

答案 9 :(得分:6)

良好的测试需要维护。

我还没有弄清楚如何在复杂环境中这样做。

随着您的代码库开始到达,所有教科书都开始脱口而出 进入数百个1000或数百行代码。

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

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

这是您开始处理权衡的地方:

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

您还需要决定:

您在哪里将测试用例存储在代码库中?

  • 您如何记录测试用例?
  • 可以重新使用测试夹具来保存测试用例维护吗?
  • 当夜间测试用例执行失败时会发生什么?分流是谁做的?
  • 如何维护模拟对象?如果您有20个模块都使用自己的模拟日志记录API,那么快速更改API会发出涟漪声。测试用例不仅会改变,而且20个模拟对象也会发生变化。这20个模块是由许多不同的团队在几年内编写的。这是一个经典的重复使用问题。
  • 个人及其团队了解自动化测试的价值,他们不喜欢其他团队的工作方式。 : - )

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

测试需要维护。

答案 10 :(得分:5)

我在This MSDN Magazine article中讨论了这些原则,我认为这对任何开发人员都很重要。

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

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

答案 11 :(得分:4)

  • 单元测试只测试单元的外部API,不应测试内部行为。
  • TestCase的每个测试都应测试此API中的一个(且仅一个)方法。
    • 对于失败案例,应包括附加的测试用例。
  • 测试测试的覆盖范围:一旦测试了一个单元,该单元内100%的线路应该已被执行。

答案 12 :(得分:2)

Jay Fields有关于写作单元测试的lot of good advices,并且有a post where he summarize the most important advices。在那里你会读到你应该批判性地考虑你的背景,并判断这些建议是否值得你。你在这里得到了大量惊人的答案,但由你自己决定哪种方式最适合你的背景。试试它们,如果它闻起来不好就重构一下。

亲切的问候

答案 13 :(得分:1)

永远不要假设一个简单的2行方法可行。编写快速单元测试是防止丢失空测试,错位减号和/或微小范围错误咬你的唯一方法,不可避免地,当你处理它的时间比现在更少时。

答案 14 :(得分:1)

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

为什么?

DRY - 不要重复自己 - 也适用于测试!测试依赖性可以帮助1)节省设置时间,2)节省夹具资源,以及3)精确定位到故障。当然,只有你的测试框架支持一流的依赖。否则,我承认,他们很糟糕。

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

答案 15 :(得分:0)

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

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

每个部分都使用Upper Camel Case,并以低分为界。

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

如果测试中的方法已经过载,我还会将参数附加到方法名称。

答案 16 :(得分:0)

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

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

答案 17 :(得分:0)

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

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

重点是避免重播所有以便能够测试每个功能。

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