单元测试命名最佳实践

647

如何为单元测试类和测试方法命名最佳实践是什么?

这个问题曾在SO上讨论过,在Unit Tests的一些流行命名约定是什么?

我不知道这是否是一个非常好的方法,但目前在我的测试项目中,我将每个生产类与一个测试类进行一对一映射,例如ProductProductTest

在我的测试类中,我使用正在测试的方法的名称、下划线以及情况和我期望发生的情况来命名方法,例如Save_ShouldThrowExceptionWithNullName()


1
请参阅这里:https://dev59.com/93VD5IYBdhLWcg3wE3Xz - Forgotten Semicolon
2
这并不是回答你的问题,但值得一读:http://haacked.com/archive/2012/01/02/structuring-unit-tests.aspx - Robs
6
GoogleйӘҺж әжЊ‡еҚ—е»ғи®®дҢүз”Ёtest<MethodUnderTest>_<state>зљ„е‘ҢеђҚж–№еәЏпәЊдң‹е¦‚testPop_emptyStackгЂ‚е…·дҢ“иҮ¦жѓ…иҮ·и§Ѓhttps://google-styleguide.googlecode.com/svn/trunk/javaguide.htmlдё­зљ„5.2.3节“方法еђҚвЂқ。如жһњжњ‰з–‘й—®пәЊиҮ·йЃµеңҒGoogleзљ„е»ғи®®гЂ‚ - Ciro Santilli OurBigBook.com
11个回答

607

更新(2021年7月)

距离我最初的回答已经有一段时间了(接近12年),在此期间,最佳实践已经发生了很大变化。因此,我觉得有必要更新我的答案,并向读者提供不同的命名策略。

许多评论和答案指出,我在原始答案中提出的命名策略不耐重构,并且最终会导致难以理解的名称,我完全同意。

在过去几年中,我采用了更人性化的命名模式,其中测试名称描述了我们想要测试的内容,正如 Vladimir Khorikov 所描述的那样。

以下是一些示例:

  • Add_credit_updates_customer_balance
  • Purchase_without_funds_is_not_possible
  • Add_affiliate_discount

但是,正如您所看到的,这是一个相当灵活的模式,最重要的是,在阅读名称时,您知道测试是关于什么的,而不包括可能随时间变化的技术细节。

对于项目和测试类的命名,我仍然遵循原始答案的模式。

原始答案(2009年10月)

我喜欢Roy Osherove的命名策略。它是这样的:

[UnitOfWork_StateUnderTest_ExpectedBehavior]

它在方法名称中提供了所有必要的信息,并且以结构化的方式呈现。

工作单元可以是一个方法,一个类甚至多个类。它应该代表在此测试用例中要测试和可控制的所有内容。

对于程序集,我使用典型的.Tests结尾,我认为这相当普遍,并且对于类(以Tests结尾)也是如此:

[NameOfTheClassUnderTestTests]

以前,我使用Fixture作为后缀而不是Tests,但我认为后者更常见,因此我改变了命名策略。


258
对我来说,在测试方法中放置方法名称毫无意义。如果您重命名方法,将会怎样?没有重构工具会为您重命名测试。最终您将不得不手动重命名测试方法,或者更有可能拥有错误命名的测试。这就像注释一样,注释过多比不注释代码更糟糕。 - Piotr Perak
96
@Peri,我认为这是一种权衡。一方面,你的测试名称可能会过时,另一方面,你无法确定你的测试在测试什么方法。我发现后者更常见。 - Joel McBeth
17
补充Peri的评论 - 所有的方法都负责某些操作,例如 UpdateManager.Update()。考虑到这一点,我倾向于将我的测试命名为 WhenUpdating_State_BehaviourWhenUpdating_Behaviour_State。这样,我可以在测试类的特定操作,同时避免在测试名称中放置方法名称。但最重要的是,当我看到一个失败测试的名称时,我必须知道哪个业务逻辑出了问题。 - Ramunas
9
如果您使用Resharper或IntelliJ重构/重命名代码时,它们都很可能会找到您的测试方法,并为您提供重命名选项。它们也会尝试查看您在注释中提到的方法名称,并更新相应的部分。 - Jeff Martin
70
好的方法名通常与方法执行的动作相同。如果你必须在使用方法名称或动作名称来命名测试时做出选择,那么这可能是提示你应该重命名方法的信号。(不过,并非每种情况都是如此。) - Kaadzia
显示剩余14条评论

151

我喜欢遵循"应该"的命名标准来为测试命名,同时在为单元测试(即类)命名测试夹具时,以受测单元为基础。

举个例子(使用C#和NUnit):

[TestFixture]
public class BankAccountTests
{
  [Test]
  public void Should_Increase_Balance_When_Deposit_Is_Made()
  {
     var bankAccount = new BankAccount();
     bankAccount.Deposit(100);
     Assert.That(bankAccount.Balance, Is.EqualTo(100));
  }
}

为什么要使用"Should"

我发现它迫使测试编写者用类似于“应该[在某种状态之后/之前/当] [执行某个操作时]”的句子来命名测试。

是的,到处写"Should"的确有点重复,但正如我所说的,它迫使作者以正确的方式思考(因此对新手可能有好处)。此外,它通常会产生可读性强的英语测试名称。

更新:

我注意到Jimmy Bogard也喜欢使用'should',甚至还有一个名为Should的单元测试库。

更新(四年后...)

对于那些感兴趣的人,我的测试命名方法随着时间的推移而发展。上述的Should模式存在一个问题,就是很难一眼看出正在测试哪个方法。对于OOP,我认为以被测试的方法开头更有意义。对于设计良好的类,这应该会产生可读性强的测试方法名称。我现在使用类似于<method>_Should<expected>_When<condition>的格式。显然,根据上下文,您可能需要将Should/When动词替换为更合适的内容。例如:Deposit_ShouldIncreaseBalance_WhenGivenPositiveValue()


46
如果测试成功,就写一句话简要说明函数的作用:当存款被进行时增加余额。 - hotshot309
3
最近看到一篇文章提到了类似的命名规范(但我没有收藏它)。这种规范旨在使按测试夹具排序后的测试列表易于阅读。你会看到像“BankAccount”这样的东西,然后在它下面(不同行)是“当进行存款时应增加余额”“当进行取款时应减少余额”等。这读起来很像一个规范,而 TDD 大致上也是关于这个的。 - Simon Elms
找到了这篇文章。它在Justin Etheredge的CodeThinked博客中使用Moq 3开始模拟-第一部分 - Simon Elms
13
我也使用Should和When,但是相反的顺序。例如:WhenCustomerDoesNotExist_ShouldThrowException()。对我而言,这比先使用Should再用When更有意义(即在某种情景下应该有一定的预期结果)。这也符合AAA(安排,行动,断言)模式......断言在结尾,而不是开头;-) - bytedev
2
@Schneider:考虑到“should”=“recommended”,那么可选的,我想知道:使用“shall”=“must”然后是必须的/强制性的不是在词汇上更好吗?例如,RFCs区分了两者。因此,测试是否通过是推荐还是必需的? - blackwizard
显示剩余3条评论

89

我喜欢这种命名风格:

OrdersShouldBeCreated();
OrdersWithNoProductsShouldFail();

等等。

它确实能让非测试人员清楚地了解问题所在。


64
但是@hotshot309,他可能正在使用.NET - .NET资本化约定 - Ace
2
@Ace,我完全同意您的观点,并在发布此评论后一分钟内注意到了这一点。当我看到我的错误时,我发誓已经将其删除,但不知何故,似乎我没有这样做。对此感到抱歉。 - hotshot309
3
@CoffeeAddict,因为在 C# 中,标识符中的下划线不是很通用,所以它们算是一个异常情况。 - Sklivvz
2
我也喜欢避免使用 should ,我更愿意使用 will,所以会写成 OrdersWithNoProductsWillFail() - Calin
6
在我看来,使用“将会(Will)”并不是很合适,这样做实际上会误导读者认为测试不可能失败...如果你用“将会”来表示未来可能发生的事情,那么你就使用得不正确了。而在这里,“应该(Should)”是更好的选择,因为它表明你希望或期望某些事情发生,但最终未能实现。当测试运行时,它会告诉你它是成功还是失败,所以你不能提前暗示任何东西,这是逻辑上的解释,那么你为什么要避免使用“应该”呢? - eyalalonn
显示剩余3条评论

56

肯特·贝克建议:

  • 每个“单元”(你的程序类)一个测试夹具。测试夹具本身也是类。测试夹具名称应为:

[name of your 'unit']Tests
测试用例(测试夹具方法)的名称如下:
test[feature being tested]

例如,有以下类:

class Person {
    int calculateAge() { ... }

    // other methods and properties
}

一个测试夹具将是:

class PersonTests {

    testAgeCalculationWithNoBirthDate() { ... }

    // or

    testCalculateAge() { ... }
}

5
我愿更多人遵循这些指南。不久之前,我不得不重新命名20多个测试方法,因为它们都有类似于“ATest”,“BasicTest”或“ErrorTest”的名称。 - Wedge
91
考虑到类名的后缀,'test' 方法前缀是否变得多余? - Gavin Miller
55
请记住,肯特写那本书时,“属性”这个概念还没有被发明。因此,方法名中的“测试”一词表明该方法是一个测试,以便测试框架进行识别。自2002年以来已经发生了很多事情。 - Thomas Jespersen
14
testCalculateAge... 这是您的测试方法的一个毫无意义的名称。 "test" 是多余的(您是否给所有方法都加上 "method" 前缀?)。其余部分没有任何测试条件或预期结果的说明。CalculateAge 是被测试的方法吗?......谁知道呢...... - bytedev
1
@ThomasJespersen 是的,但现在批评也不会变得无效。不再需要使用前缀了。 - Blorgbeard
显示剩余2条评论

19

类名。对于测试夹具名称,我发现在许多领域的通用语言中,“Test”是很常见的。例如,在工程领域:StressTest,在化妆品领域:SkinTest。抱歉不同意肯特的看法,但在我的测试夹具中使用“Test”(StressTestTest?)会让人困惑。

在许多领域中也经常使用“Unit”。例如:MeasurementUnit。一个名为MeasurementUnitTest的类是对“Measurement”还是“MeasurementUnit”的测试呢?

因此,我喜欢在所有的测试类中使用“Qa”前缀。例如:QaSkinTestQaMeasurementUnit。它永远不会与领域对象混淆,而且使用前缀而不是后缀意味着所有测试夹具在视觉上都在一起(如果你在测试项目中有伪造或其他支持类,这非常有用)。

命名空间。我在C#中工作,并将我的测试类放在与它们测试的类相同的命名空间中。这比拥有单独的测试命名空间更方便。当然,测试类位于不同的项目中。

测试方法名称。我喜欢将我的方法命名为WhenXXX_ExpectYYY。这使得前置条件明确,并有助于自动化文档(例如TestDox)。这类似于Google测试博客上的建议,但具有更多的前置条件和期望值分离。例如:

WhenDivisorIsNonZero_ExpectDivisionResult
WhenDivisorIsZero_ExpectError
WhenInventoryIsBelowOrderQty_ExpectBackOrder
WhenInventoryIsAboveOrderQty_ExpectReducedInventory

你谈到了测试方法名称和测试夹具名称。测试夹具名称映射到生产类。在测试中,你在哪里编写生产方法名称? - The Light

14

Given-When-Then和MethodName_Scenario_ExpectedBehavior是一样的,不是吗?! - The Light
2
不完全是。Given_When_Then更多地指: 给定一个实体_当某些操作发生_期望得到那个结果测试应该表达对行为而非实现的测试意愿 - plog17
1
“Given When Then”通常被称为Gherkin。它是Cucumber、JBehave和Behat工具中产生的DSL。 - bytedev
+1 这是我认为最好的方法。将方法执行的方式与您期望的结果分离开来,非常强大,可以避免其他评论中描述的许多问题。 - Lee
像这样吗?给定已登录_当写文章_然后显示成功消息 - wonsuc

10
我最近想出了以下的命名规则来命名我的测试、它们的类和包含的项目,以最大化它们的描述性:
假设我正在测试MyApp.Serialization 命名空间下的 Settings 类。
首先,我将创建一个名为MyApp.Serialization.Tests 的测试项目。
在这个项目中,当然也是在该命名空间中,我会创建一个名为 IfSettings 的类(保存为 IfSettings.cs )。
比如我正在测试SaveStrings() 方法。-> 我会将测试命名为 CanSaveStrings()
当我运行这个测试时,它将显示以下标题: MyApp.Serialization.Tests.IfSettings.CanSaveStrings 我认为这很好地告诉我,它正在测试什么。
当然,在英语中,“Tests”这个名词和“tests”这个动词是相同的,这是有用的。
你可以用任何创造性的方式来命名测试,以便我们得到完整的句子标题。
通常,测试名称必须以动词开头。
例如:
  • Detects (例如DetectsInvalidUserInput)
  • Throws (例如ThrowsOnNotFound)
  • Will (例如WillCloseTheDatabaseAfterTheTransaction)
  • 等等。
    另一种选择是使用“that”而不是“if”。
    后者虽然节省了键盘输入,但更准确地描述了我正在做什么,因为我不知道被测试的行为是否存在,但正在测试它是否存在。
    [编辑]
    现在使用上述命名约定后,我发现在处理接口时,“If”前缀可能会引起混淆。假设测试类 IfSerializer.cs 看起来非常类似于“打开文件”标签中的接口 ISerializer.cs
    当在测试、被测试的类和它的接口之间来回切换时,这可能会非常烦人。因此,作为前缀,我现在会选择 That 而不是 If
    此外,我现在只在我的测试类中使用“_”来分隔测试方法名称中的单词,因为在其他任何地方都不被视为最佳实践 - 例如:
    [Test] public void detects_invalid_User_Input()
    

    我发现这样更容易阅读。

    [结束编辑]

    我希望这能激发出更多想法,因为我认为给测试命名非常重要,它可以节省你大量的时间,否则这些时间将会花费在试图理解测试正在做什么上(例如在长期停顿后恢复项目)。


    9

    7

    我认为最重要的一点是在命名约定上保持一致(并与团队中的其他成员达成一致)。太多时候,我看到同一个项目中使用了许多不同的约定。


    2

    我应该补充一下,将测试代码放在与被测试的源代码相同的包中,但是在平行目录中,可以消除代码膨胀问题,当你准备部署代码时,无需进行大量排除模式操作。

    我个人喜欢《JUnit Pocket Guide》中描述的最佳实践...很难超越JUnit的联合创始人所写的书!


    3
    不要相信这实际上回答了手头的问题-你能否进行编辑并参考JUnit Pocket Guide?谢谢! - Nate-Wilkins

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接