5

我正在尝试行为驱动的开发,我发现自己在编写设计时第二次猜测我的设计。这是我的第一个绿地项目,可能只是我缺乏经验。无论如何,这是我正在编写的类的简单规范。它以 BDD 风格用 NUnit 编写,而不是使用专用的行为驱动框架。这是因为该项目以 .NET 2.0 为目标,并且所有 BDD 框架似乎都采用了 .NET 3.5。

[TestFixture]
public class WhenUserAddsAccount
{
    private DynamicMock _mockMainView;
    private IMainView _mainView;

    private DynamicMock _mockAccountService;
    private IAccountService _accountService;

    private DynamicMock _mockAccount;
    private IAccount _account;

    [SetUp]
    public void Setup()
    {
        _mockMainView = new DynamicMock(typeof(IMainView));
        _mainView = (IMainView) _mockMainView.MockInstance;

        _mockAccountService = new DynamicMock(typeof(IAccountService));
        _accountService = (IAccountService) _mockAccountService.MockInstance;

        _mockAccount = new DynamicMock(typeof(IAccount));
        _account = (IAccount)_mockAccount.MockInstance;
    }

    [Test]
    public void ShouldCreateNewAccount()
    {
        _mockAccountService.ExpectAndReturn("Create", _account);
        MainPresenter mainPresenter = new MainPresenter(_mainView, _accountService);
        mainPresenter.AddAccount();
        _mockAccountService.Verify();
    }
}

MainPresenter 使用的接口都没有任何真正的实现。AccountService 将负责创建新帐户。可以有多个 IAccount 实现定义为单独的插件。在运行时,如果有多个,则会提示用户选择要创建的帐户类型。否则 AccountService 将简单地创建一个帐户。

让我感到不安的一件事是编写一个规范/测试需要多少模拟。这只是使用 BDD 的副作用还是我以错误的方式处理这件事?

[更新]

这是 MainPresenter.AddAccount 的当前实现

    public void AddAccount()
    {
        IAccount account;
        if (AccountService.AccountTypes.Count == 1)
        {
            account = AccountService.Create();
        }
        _view.Accounts.Add(account);
    }

欢迎任何提示、建议或替代方案。

4

7 回答 7

3

在进行自上而下的开发时,发现自己使用大量模拟是很常见的。你需要的部分不在那里,所以你需要模拟它们。话虽如此,这确实感觉像是一个验收水平测试。根据我的经验,BDD 或上下文/规范在单元测试级别开始变得有点奇怪。在单元测试级别,我可能会做更多的事情......

when_adding_an_account
   should_use_account_service_to_create_new_account
   should_update_screen_with_new_account_details

您可能需要重新考虑对 IAccount 接口的使用。我个人坚持将服务接口保留在域实体之上。但这更多是个人喜好。

其他一些小建议...

  • 您可能需要考虑使用 Mocking 框架,例如 Rhino Mocks(或 Moq),它允许您避免在断言中使用字符串。
  _mockAccountService.Expect(mock => mock.Create())
     .return(_account);

  • 如果您使用 BDD 样式,我见过的一种常见模式是使用链式类进行测试设置。在你的例子中......
公共类 MainPresenterSpec
{
    // Mocks 的受保护变量

    [设置]
    公共无效设置()
    {
       // 设置模拟
    }

}

[测试夹具]
公共类WhenUserAddsAccount:MainPresenterSpec
{
    [测试]
    公共无效应该创建新帐户()
    {
    }
}
  • 另外,我建议您更改代码以使用保护子句..
     公共无效 AddAccount()
     {
        if (AccountService.AccountTypes.Count != 1)
        {
            // 在这里做任何你想做的事。发消息?
        返回;
        }

    IAccount 帐户 = AccountService.Create();

        _view.Accounts.Add(帐户);
     }
于 2009-06-10T19:23:16.633 回答
2

如果您使用自动模拟容器,例如 RhinoAutoMocker(StructureMap的一部分),测试生命支持会简单得多。您使用自动模拟容器来创建被测类,并要求它提供测试所需的依赖项。容器可能需要在构造函数中注入 20 个东西,但如果你只需要测试一个,你只需要请求那个。

using StructureMap.AutoMocking;

namespace Foo.Business.UnitTests
{
    public class MainPresenterTests
    {
        public class When_asked_to_add_an_account
        {
            private IAccountService _accountService;
            private IAccount _account;
            private MainPresenter _mainPresenter;

            [SetUp]
            public void BeforeEachTest()
            {
                var mocker = new RhinoAutoMocker<MainPresenter>();
                _mainPresenter = mocker.ClassUnderTest;
                _accountService = mocker.Get<IAccountService>();
                _account = MockRepository.GenerateStub<IAccount>();
            }

            [TearDown]
            public void AfterEachTest()
            {
                _accountService.VerifyAllExpectations();
            }

            [Test]
            public void Should_use_the_AccountService_to_create_an_account()
            {
                _accountService.Expect(x => x.Create()).Return(_account);
                _mainPresenter.AddAccount();
            }
        }
    }
}

在结构上,我更喜欢在单词之间使用下划线而不是 RunningThemAllTogether,因为我发现它更容易扫描。我还创建了一个以被测类命名的外部类和多个以被测方法命名的内部类。然后,测试方法允许您指定被测方法的行为。在 NUnit 中运行时,它会为您提供如下上下文:

Foo.Business.UnitTests.MainPresenterTest
  When_asked_to_add_an_account
    Should_use_the_AccountService_to_create_an_account
    Should_add_the_Account_to_the_View
于 2009-06-10T22:06:35.290 回答
1

对于具有应该交还帐户的服务的演示者来说,这似乎是正确的模拟数量。

不过,这似乎更像是一个验收测试而不是一个单元测试——也许如果你降低断言的复杂性,你会发现一组较小的关注点被嘲笑。

于 2009-06-10T15:49:38.767 回答
1

是的,你的设计有缺陷。您正在使用模拟 :)

更严重的是,我同意之前的发帖人建议你的设计应该分层,这样每一层都可以单独测试。我认为原则上测试代码应该改变实际的生产代码是错误的——除非这可以自动和透明地完成,因为可以编译代码以进行调试或发布。

这就像海森堡的不确定性原理——一旦你有了模拟,你的代码就被如此改变,它变成了一个令人头疼的维护问题,而且模拟本身有可能引入或掩盖错误。

如果你有干净的接口,我不会反对实现一个简单的接口来模拟(或模拟)另一个模块的未实现接口。这种模拟可以以与模拟相同的方式用于单元测试等。

于 2009-06-10T23:39:50.017 回答
0

您可能希望使用MockContainers来摆脱所有模拟管理,同时创建演示者。它大大简化了单元测试。

于 2009-06-10T19:32:05.867 回答
0

这没关系,但我希望在某个地方有一个 IoC 自动模拟容器。代码提示测试编写者手动(显式)在测试中的模拟对象和真实对象之间切换,这不应该是这种情况,因为如果我们谈论的是单元测试(单元只是一个类),自动模拟更简单所有其他类并使用模拟。

我想说的是,如果您有一个同时使用mainViewand的测试类mockMainView,那么您就没有严格意义上的单元测试——更像是集成测试。

于 2009-06-10T19:33:05.053 回答
0

我认为,如果您发现自己需要模拟,那么您的设计是不正确的。

组件应该分层。您单独构建和测试组件 A。然后你构建和测试 B+A。一旦满意,您就构建 C 层并测试 C+B+A。

在您的情况下,您不需要“_mockAccountService”。如果您的真实 AccountService 已经过测试,则只需使用它即可。这样你就知道任何错误都在 MainPresentor 中,而不是在 mock 本身中。

如果您的真实 AccountService 尚未经过测试,请停止。返回并执行所需的操作以确保其正常工作。让它达到你可以真正依赖它的程度,那么你就不需要模拟了。

于 2009-06-10T23:20:43.240 回答