我真的需要一个单元测试框架吗?


19

目前,我的工作是为C ++应用程序提供大量的单元测试。但是,我们不使用单元测试框架。他们只是利用了一个C宏,它基本上包装了一个断言和一个cout。就像是:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

然后,我们只需为每个测试创建函数,例如

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

我们的CI服务器会选择“单元测试失败”,并且构建失败,并通过电子邮件将消息发送给开发人员。

如果我们有重复的设置代码,我们可以像在生产中使用的任何其他重复代码一样简单地对其进行重构。我们将其包装在辅助函数的后面,使一些测试类包装设置常用的场景。

我知道那里有CppUnit和boost单元测试之类的框架。我想知道这些增加了什么价值?我想念这些带来的好处吗?我可以从中受益吗?我不愿意添加一个依赖项,除非它增加了实际价值,尤其是因为看起来我们拥有的东西简直太简单了并且运作良好。

Answers:


8

正如其他人已经说过的那样,您已经拥有了自己的简单家庭自制框架。

做一个似乎微不足道。但是,单元测试框架的其他一些功能不太容易实现,因为它们需要一些高级语言知识。我通常需要测试框架提供的功能,而这些功能不太容易自制:

  • 自动收集测试用例。即,定义新的测试方法应该足以使其执行。JUnit自动收集名称以开头的所有方法test,NUnit带有[Test]注释,Boost.Test使用BOOST_AUTO_TEST_CASEBOOST_FIXTURE_TEST_CASE宏。

    这主要是方便,但是您可以获得的每一个便利都会提高开发人员实际编写他们应该编写的测试并正确连接它们的机会。如果您有很长的说明,那么现在有人会错过其中的一部分,然后可能无法进行某些测试,并且没人会注意到。

  • 能够运行选定的测试用例,而无需调整代码和重新编译。任何体面的单元测试框架都允许您指定要在命令行上运行的测试。如果要调试单元测试(对于许多开发人员而言,这是最重要的一点),则需要能够选择一些要运行的代码,而无需在各处调整代码。

    假设您刚刚收到了错误报告#4211,并且可以通过单元测试进行复制。因此,您编写了一个,但是您不需要告诉跑步者只运行该测试,因此您可以调试实际存在的错误。

  • 能够对每个测试用例标记测试预期的失败,而无需修改检查本身。实际上,我们在工作中切换了框架以获取这一框架。

    任何大小合适的测试套件都将进行测试,这些测试将失败,因为它们所测试的功能尚未实现,尚未完成,没有人有时间来修复它们或其他东西。如果没有能力将测试标记为预期的失败,则当定期出现某些失败时,您将不会注意到另一个失败,因此这些测试不再达到其主要目的。


谢谢,我认为这是最好的答案。现在,我的宏可以完成工作,但是我无法执行您提到的任何功能。
Doug T.

1
@Jan Hudec“这主要是方便,但是您可以获得的每一个便利都会提高开发人员实际编写他们应该编写的测试并正确连接它们的机会。” 所有的测试框架都是(1)安装简单,与最新有效说明相比,通常具有更多的过时或非详尽安装说明;(2)如果您直接提交测试框架,而中间没有接口,则说明您已经结婚了,切换框架并不总是那么容易。
德米特里

@Jan Hudec如果我们希望有更多的人编写单元测试,那么我们在Google上对于“什么是单元测试”必须比“什么是单元测试”获得更多的结果。如果您不知道什么单元测试独立于任何单元测试框架或单元测试的定义,则没有必要进行单元测试。除非您对什么是单元测试有深入的了解,否则您无法进行单元测试,否则将没有进行单元测试的意义。
德米特里

我不赞成这种方便的说法。如果您离开了琐碎的示例世界,那么编写测试代码将非常困难。所有这些样机,设置,库,外部样机服务器程序等。它们都要求您从内而外了解测试框架。
Lothar

@Lothar,是的,这是很多工作,需要学习的东西很多,但是仍然要一遍又一遍地编写简单的样板,因为您缺少几个有用的实用程序,这会使工作变得不那么愉快,并且在效果上也有明显的不同。
Jan Hudec

27

似乎您已经在使用一个自制的框架。

更流行的框架的附加价值是什么?我要说的是,它们增加的价值是,当您必须与公司外部的人员交换代码时,您可以做到,因为它基于已知且广泛使用的框架。

另一方面,自制框架会迫使您从不共享您的代码或提供框架本身,这可能会随着框架本身的增长而变得繁琐。

如果您按原样将代码提供给同事,没有任何解释,也没有单元测试框架,那么他将无法编译该代码。

自制框架的第二个缺点是兼容性。流行的单元测试框架倾向于确保与不同的IDE,版本控制系统等兼容。目前,这对您而言可能并不十分重要,但是如果一天您需要在CI服务器中进行某些更改或迁移,将会发生什么情况?到新的IDE或新的VCS?你会重新发明轮子吗?

最后但并非最不重要的一点是,大型框架有一天会提供您可能需要在自己的框架中实现的更多功能Assert.AreEqual(expected, actual)并不总是足够的。如果您需要:

  • 测量精度?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • 无效测试,如果运行时间太长?即使在促进异步编程的语言中,重新实现超时也不是一件容易的事。

  • 测试一个期望引发异常的方法?

  • 有更优雅的代码?

    Assert.Verify(a == null);
    

    很好,但这不是更能表达您写下一行的意图吗?

    Assert.IsNull(a);
    

我们使用的“框架”都在一个很小的头文件中,并且遵循assert的语义。因此,我不太担心您列出的缺点。
Doug T.

4
我认为断言是测试框架中最琐碎的部分。收集并运行测试用例并检查结果的跑步者是至关重要的部分。
Jan Hudec 2012年

@Jan我不太了解。我的跑步者是每个C ++程序共有的主要例程。单元测试框架运行器是否做得更复杂,更有用?
Doug T.

1
到目前为止,您的框架仅允许在主要方法中使用断言和运行测试的语义。只要等到你有你的断言为多个场景组,组相关的场景一起根据初始化的数据,等等
詹姆斯Kingsbery

@DougT .:是的,体面的单元测试框架运行程序做了一些更复杂的有用的事情。查看我的完整答案。
Jan Hudec 2012年

4

正如其他人已经说过的那样,您已经拥有自己的自制框架。

我看到使用其他测试框架的唯一原因是从行业“常识”的角度。新的开发人员不必学习您的自制方法(尽管看起来很简单)。

另外,其他测试框架可能具有更多可以利用的功能。


1
同意 如果您当前的测试策略没有遇到限制,那么我几乎没有理由进行更改。一个好的框架可能会提供更好的组织和报告功能,但是您必须证明与代码库(包括构建系统)集成所需的其他工作是合理的。
TMN 2012年

3

即使已经是一个简单的框架,您也已经有了。

在我看来,更大的框架的主要优点是能够具有许多不同类型的断言(例如,断言引发),对单元测试的逻辑顺序以及仅在单元测试中运行子集的能力。一个时间。另外,如果可以的话,xUnit测试的模式也很不错-例如setUP()和tearDown()。当然,这将您锁定在上述框架中。请注意,某些框架比其他框架具有更好的模拟集成-例如google模拟和测试。

您需要花费多长时间将所有单元测试重构为新框架?几天或几周也许值得,但更多的可能就不那么值得了。


2

我的看法是,你们俩都有优势,而你们处于“劣势”(原文如此)。

好处是您拥有一个舒适的系统,并且可以为您工作。您很高兴它确认了您产品的有效性,并且尝试为使用不同框架的产品更改所有测试时,可能会发现没有商业价值。如果您可以重构代码,并且测试可以接受更改,或者更好,但是,如果您可以修改测试,并且现有代码在重构之前无法通过测试,那么您已经涵盖了所有基础。然而...

拥有设计良好的单元测试API的优点之一是,在大多数现代IDE中都有很多本机支持。这不会影响那些对Visual Studio用户冷嘲热讽的核心VI和emacs用户,但是对于那些使用良好IDE的使用者,您可以调试测试并在其中执行IDE本身。这样做很好,但是根据您使用的框架,还有一个更大的优势,那就是用来测试代码的语言

当我说语言时,我不是在谈论编程语言,而是在谈论用流利的语法包装的丰富的单词,使测试代码读起来像个故事。特别是,我已经成为使用BDD框架的拥护者。我个人最喜欢的DotNet BDD API是StoryQ,但是还有其他几个具有相同的基本目的,那就是从需求文档中取出一个概念,并以与规范中的编写方式相似的方式将其编写为代码。然而,真正好的API可以走得更远,可以拦截测试中的每个单独的语句,并指示该语句是成功执行还是失败。这非常有用,因为您可以看到整个测试已执行而没有提前返回,这意味着调试工作变得非常高效,因为您只需要将注意力集中在测试失败的部分上,而无需解码整个调用顺序。另一个好处是,测试输出将向您显示所有这些信息,

作为我正在谈论的示例,请比较以下内容:

使用断言:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

使用流畅的BDD API :( 假设斜体位基本上是方法指针)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

现在授予BDD语法更长,更单词,并且这些示例都非常人为,但是对于非常复杂的测试情况,由于给定的系统行为,系统中的许多事情都在变化,BDD语法为您提供了清晰的关于您正在测试的内容以及如何定义测试配置的描述,您可以将该代码显示给非程序员,他们将立即了解正在发生的事情。此外,如果“ variable_A”在两种情况下均未通过测试,则在您解决问题之前,Asserts示例将不会执行第一个断言,而BDD API会依次执行链中调用的每个方法,并指出语句的各个部分有误。

就我个人而言,我发现这种方法比更传统的xUnit框架要好得多,因为测试的语言与客户说出他们的逻辑要求的语言相同。即便如此,我还是设法以相似的样式使用xUnit框架,而无需发明一个完整的测试API来支持我的工作,尽管这些assert仍然可以有效地使自己短路,但是它们的阅读更加清晰。例如:

使用Nunit

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

如果您决定使用单元测试API进行探索,我的建议是在一段时间内尝试使用大量不同的API,并保持开放态度。虽然我本人主张使用BDD,但您自己的业务需求可能会因团队情况而有所不同。但是,关键是要避免对现有系统进行猜测。如果需要,您总是可以通过使用其他API的一些测试来支持现有的测试,但是我当然不建议为了使所有内容相同而进行大量的测试重写。由于遗留代码不再使用,您可以轻松地用新代码替换它及其测试,并使用替代API进行测试,而无需花费大量精力来投入,这不一定会给您带来真正的商业价值。至于使用单元测试API,


1

您所拥有的很简单,就能完成工作。如果它适合您,那就太好了。您不需要主流的单元测试框架,而我会毫不犹豫地将现有的单元测试库移植到新框架上。我认为单元测试框架的最大价值在于减少进入门槛。您只需开始编写测试,因为该框架已经存在。您已经超过了这一点,因此您将无法获得好处。

使用主流框架的另一个好处是IMO的次要好处是,新开发人员可能已经加快了使用任何框架的速度,因此需要的培训更少。在实践中,采用您所描述的简单方法,这没什么大不了的。

而且,大多数主流框架都具有您的框架可能具有或不具有的某些功能。这些功能减少了管道代码,并使编写测试用例的速度更快,更容易:

  • 通过使用命名约定,注释/属性等自动运行测试用例。
  • 各种更具体的断言,因此您不必为所有断言编写条件逻辑或捕获异常来断言其类型。
  • 测试用例的分类,因此您可以轻松地运行它们的子集。
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.