【问题标题】:How to properly mock and unit test如何正确模拟和单元测试
【发布时间】:2009-01-24 19:40:15
【问题描述】:

我基本上是在尝试自学如何编码,并且我想遵循良好的做法。单元测试有明显的好处。在单元测试方面也有很多狂热,我更喜欢更实用的编码和生活方法。作为上下文,我目前正在编写我的第一个“真实”应用程序,它是使用 asp.net MVC 的无处不在的博客引擎。我通过自己的调整松散地遵循 MVC Storefront 架构。因此,这是我第一次真正尝试模拟对象。我将代码示例放在问题的末尾。

如果我有任何见解或外部资源,我将不胜感激,以增加我对测试和模拟基础知识的理解。我在网上找到的资源通常是针对嘲笑的“方式”,我需要更多地了解嘲笑的地点、原因和时间。如果这不是问这个问题的最佳地点,请指点我一个更好的地方。

我试图了解我从以下测试中获得的价值。 UserService 依赖于 IUserRepository。服务层的价值是将逻辑与数据存储分开,但在这种情况下,大多数 UserService 调用只是直接传递给 IUserRepository。没有太多实际逻辑可以测试的事实也可能是我担心的根源。我有以下顾虑。

  • 感觉代码只是在测试模拟框架是否正常工作。
  • 为了模拟出依赖关系,这使我的测试对 IUserRepository 实现有太多了解。这是必要的邪恶吗?
  • 我实际上从这些测试中获得了什么价值?被测服务的简单性是否让我怀疑这些测试的价值。

我正在使用 NUnit 和 Rhino.Mocks,但我想要完成的工作应该相当明显。

    [SetUp]
    public void Setup()
    {
        userRepo = MockRepository.GenerateMock<IUserRepository>();
        userSvc = new UserService(userRepo);
        theUser = new User
        {
            ID = null,
            UserName = "http://joe.myopenid.com",
            EmailAddress = "joe@joeblow.com",
            DisplayName = "Joe Blow",
            Website = "http://joeblow.com"
        };
    }

    [Test]
    public void UserService_can_create_a_new_user()
    {
        // Arrange
        userRepo.Expect(repo => repo.CreateUser(theUser)).Return(true);

        // Act
        bool result = userSvc.CreateUser(theUser);

        // Assert
        userRepo.VerifyAllExpectations();
        Assert.That(result, Is.True, 
          "UserService.CreateUser(user) failed when it should have succeeded");
    }

    [Test]
    public void UserService_can_not_create_an_existing_user()
    {
        // Arrange
        userRepo.Stub(repo => repo.IsExistingUser(theUser)).Return(true);
        userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
        // Act
        bool result = userSvc.CreateUser(theUser);

        // Assert
        userRepo.VerifyAllExpectations();
        Assert.That(result, Is.False, 
            "UserService.CreateUser() allowed multiple copies of same user to be created");
    }

【问题讨论】:

    标签: unit-testing mocking


    【解决方案1】:

    基本上你在这里测试的是方法被调用,而不是它们是否真的有效。这是模拟应该做的。他们不调用该方法,而是检查该方法是否被调用,并返回 Return() 语句中的任何内容。所以在你的断言中:

    Assert.That(result, Is.False, "error message here");
    

    这个断言总是会成功,因为你的期望总是会返回 false,因为 Return 语句:

    userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
    

    我猜这在这种情况下没多大用处。

    模拟很有用的地方是,例如,当您想在代码中的某处进行数据库调用,但又不想实际调用数据库时。你想假装数据库被调用,但是你想设置一些假数据让它返回,然后(这是重要的部分)测试对你的模拟返回的假数据执行某些操作的逻辑。在上面的示例中,您省略了最后一步。想象一下,您有一个方法向用户显示一条消息,说明是否创建了新用户:

    public string displayMessage(bool userWasCreated) {
        if (userWasCreated)
            return "User created successfully!";
        return "User already exists";
    }
    

    那么你的测试将是

    userRepo.Expect(repo => repo.CreateUser(theUser)).Return(false);
    Assert.AreEqual("User already exists", displayMessage(userSvc.CreateUser(theUser)))
    

    现在这有一些价值,因为您正在测试一些实际行为。当然,您也可以直接通过传入“true”或“false”来测试它。您甚至不需要模拟该测试。测试预期很好,但我已经编写了很多这样的测试,并且得出了与您得出的相同结论 - 它只是没那么有用。

    因此,简而言之,当您想要抽象出外部性(如数据库或 Web 服务调用等)并在此时注入已知值时,模拟很有用。但是直接测试 mock 并不经常有用。

    【讨论】:

    • 验证行为更有意义。由于我如何设置模拟,我无法理解编写会自动通过的测试的价值。测试行为实际上可以洞察被测代码的功能。
    【解决方案2】:

    您是对的:服务的简单性使这些测试变得无趣。直到您在服务中获得更多业务逻辑,您才能从测试中获得价值。

    您可以考虑一些类似的测试:

    CreateUser_fails_if_email_is_invalid()
    CreateUser_fails_if_username_is_empty()
    

    另一条评论:它看起来像代码异味,您的方法返回布尔值以指示成功或失败。您可能有充分的理由这样做,但通常您应该让异常传播出去。这也使得编写好的测试变得更加困难,因为您将无法检测您的方法是否因“正确的原因”而失败,f.x。你可以这样写 CreateUser_fails_if_email_is_invalid()-test:

    [Test]
    public void CreateUser_fails_if_email_is_invalid()
    {
        bool result = userSvc.CreateUser(userWithInvalidEmailAddress);
        Assert.That(result, Is.False);
    }
    

    它可能适用于您现有的代码。使用 TDD Red-Green-Refactor-cycle 可以缓解这个问题,但如果能够实际检测到方法失败是因为无效的电子邮件,而不是因为其他问题,那就更好了。

    【讨论】:

    • 我确实覆盖了该服务,但我只发布了几个示例,因为它们相当多余。我确实将 bool 返回分解为异常,它确实为我提供了有关代码如何失败的更多信息。感谢您的洞察力。
    【解决方案3】:

    如果您在编写代码之前编写测试,您将从单元测试中获得更多收益。感觉你的测试不值钱的原因之一是你没有得到让你的测试驱动设计的价值。之后写你的测试只是一个练习,看看你是否能记住所有可能出错的东西。首先编写测试会让您考虑如何实际实现功能。

    这些测试并不是那么有趣,因为正在实现的功能非常基本。你进行模拟的方式似乎很标准——模拟被测类所依赖的东西,而不是被测类。可测试性(或良好的设计意识)已经引导您实现接口并使用依赖注入来减少耦合。正如其他人所建议的那样,您可能需要考虑更改错误处理。例如,如果只是为了提高测试质量,很高兴知道为什么 CreateUser 失败了。您可以使用异常或 out 参数来执行此操作(如果我没记错的话,MembershipProvider 就是这样工作的)。

    【讨论】:

    • 系统设计是我还在学习的东西。我不做“纯粹的”TDD,因为我喜欢了解系统的一小部分如何融入整体。当我的测试验证或反驳我的假设时,我会写下我认为我的接口将如何工作并重构接口。
    【解决方案4】:

    您正面临“经典”与“模拟”测试方法的问题。或者 Martin Fowler 描述的“状态验证”与“行为验证”:http://martinfowler.com/articles/mocksArentStubs.html#ClassicalAndMockistTesting

    另一个最优秀的资源是 Gerard Meszaros 的书“xUnit 测试模式:重构测试代码”

    【讨论】:

      猜你喜欢
      • 2021-11-19
      • 2021-07-04
      • 1970-01-01
      • 2019-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-12
      相关资源
      最近更新 更多