使用单元测试讲故事是个好主意吗?


13

因此,我有一段时间前编写的身份验证模块。现在,我看到了自己的错误并为此编写了单元测试。在编写单元测试时,我很难想出好名字和好地方进行测试。例如,我有类似的东西

  • 需要Login_should_redirect_when_not_logged_in
  • 需要登录时登录_通过_登录_登录
  • Login_should_work_when_given_proper_credentials

就个人而言,即使看起来“适当”,我还是觉得它有点丑陋。我也难以通过仅扫描测试来区分测试(我必须至少读取两次方法名称才能知道失败了)。

因此,我认为也许不编写纯粹测试功能的测试,而是编写一组涵盖场景的测试。

例如,这是我想出的一个测试存根:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

这是听说过的图案吗?我已经看到了接受的故事等等,但这是根本不同的。最大的区别是我提出了“强制”测试的方案。而不是手动尝试提出可能需要进行测试的交互。另外,我知道这会鼓励单元测试不完全测试一种方法和类。我认为这还可以。另外,我知道这会给至少一些测试框架带来问题,因为它们通常假定测试彼此独立并且顺序无关紧要(在这种情况下,顺序是无关紧要的)。

无论如何,这是一个明智的模式吗?还是,这非常适合我的API的集成测试,而不是“单元”测试?这只是一个个人项目,因此我可以进行可能会或可能不会顺利进行的实验。


4
单元测试,集成测试和功能测试之间的界限很模糊,如果我必须为您的测试存根选择一个名称,那么它就可以正常工作。
yannis 2013年

我认为这是一个品味问题。就我个人而言,我使用_test附有测试内容的名称,并使用注释注明预期的结果。如果是个人项目,请找到自己喜欢的风格并坚持下去。
李斯特先生,2013年

1
我已经使用Arrange / Act / Assert模式写了一个更传统的单元测试方式的详细答案,但是一个朋友使用github.com/cucumber/cucumber/wiki/Gherkin取得了很多成功,这是用于规格和afaik可以生成黄瓜测试。
StuperUser

尽管我不会使用nunit或类似方法展示的方法,但nspec支持以更多故事驱动的方式构建上下文和测试:nspec.org
Mike

1
将“帐单”更改为“用户”,您就完成了
Steven A. Lowe 2013年

Answers:


15

是的,给您的测试取一个您正在测试的示例场景的名称是个好主意。而且,将单元测试工具用于不仅仅是单元测试也可以,很多人都成功地做到了这一点(我也是)。

但是,不能,以测试执行顺序很重要的方式编写测试绝对不是一个好主意。例如,NUnit允许用户交互地选择他/她想要执行的测试,因此这将不再按预期的方式工作。

您可以通过将每个测试的主要测试部分(包括“断言”)与将系统设置为正确的初始状态的部分分开来轻松避免这种情况。使用上面的示例:编写用于创建帐户,登录并发表评论的方法-无需任何断言。然后在不同的测试中重复使用这些方法。您还必须[Setup]在测试装置的方法中添加一些代码,以确保系统处于正确定义的初始状态(例如,数据库中到目前为止没有帐户,到目前为止没有人连接等)。

编辑:当然,这似乎与测试的“故事”性质背道而驰,但是,如果为您的助手方法赋予有意义的名称,则会在每个测试中找到您的故事。

因此,它应如下所示:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}

我会进一步说,使用xUnit工具运行功能测试很好,只要您不要将工具与测试类型混淆,并且将这些测试与实际的单元测试分开,以便开发人员可以仍在提交时快速运行单元测试。这些可能比单元测试要慢得多。
bdsl

4

用单元测试讲故事的一个问题是,它并没有明确表明单元测试应该完全独立地安排和运行。

一个好的单元测试应该与所有其他依赖代码完全隔离,这是可以测试的最小代码单元

这样做的好处是,它还可以确认代码是否有效,如果测试失败,您可以免费获得准确的代码错误诊断。如果测试不是孤立的,则必须查看它所依赖的内容,以准确找出问题所在,并错过单元测试的主要好处。具有执行顺序的问题也会引起很多误报,如果测试失败,则尽管以下测试能够正常工作,但以下测试仍可能失败。

一篇更深入的好文章是有关肮脏混合测试的经典文章。

为了使类,方法和结果易于阅读,伟大的单元测试艺术使用命名约定

测试类别:

ClassUnderTestTests

测试方法:

MethodUnderTest_Condition_ExpectedResult

为了复制@Doc Brown的示例,我编写了一些辅助方法来构建要测试的隔离对象,而不是使用在每次测试之前运行的[Setup]。

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

因此,失败的测试具有一个有意义的名称,该名称为您提供了有关哪种方法失败,条件和预期结果的准确描述。

这就是我一直编写单元测试的方式,但是一个朋友在Gerkin上取得了很多成功。


1
尽管我认为这是一篇不错的文章,但我对链接文章对“混合”测试的说法持不同意见。恕我直言,拥有“小型”集成测试(当然,不能替代单元测试)对IMHO很有帮助,即使它们无法准确告诉您哪种方法包含错误的代码。如果这些测试的可维护性取决于这些测试的代码编写的干净程度,那么它们本身并不是“肮脏的”。而且我认为这些测试的目标非常明确(例如OP中的示例)。
Doc Brown

3

您所描述的听起来更像是行为驱动设计(BDD),而不是单元测试。看一下SpecFlow,这是一种基于Gherkin DSL 的.NET BDD技术。

任何人都可以在不了解编码的情况下读/写的强大功能。我们的测试团队在将其用于我们的集成测试套件中获得了巨大的成功。

关于单元测试的约定,@ DocBrown的答案似乎很可靠。


对于信息,BDD与TDD完全一样,只是写作风格在变化。示例:TDD = assert(value === expected)BDD = value.should.equals(expected)+您描述了解决“单元测试独立性”问题的层中的要素。这是一个很棒的风格!
Offirmo
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.