【问题标题】:Is this a poor design?这是一个糟糕的设计吗?
【发布时间】:2009-06-10 15:41:21
【问题描述】:

我正在尝试行为驱动的开发,但我发现自己在编写设计时会再次猜测我的设计。这是我的第一个绿地项目,可能只是我缺乏经验。无论如何,这是我正在编写的类的简单规范。它以 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);
    }

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

【问题讨论】:

    标签: c# unit-testing bdd


    【解决方案1】:

    在进行自上而下的开发时,经常会发现自己使用了很多模拟。你需要的部分不在那里,所以你需要模拟它们。话虽如此,这确实感觉像是一个验收水平测试。根据我的经验,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(帐户); }

    【讨论】:

      【解决方案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
      

      【讨论】:

      • +1 用于推荐工具以简化推荐增强功能的采用以及增强功能。
      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

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

        这就像 Heisenberg 不确定性原理 - 一旦你在其中有了 mock,你的代码就会发生如此大的改变,以至于成为维护方面的难题,而且 mock 本身有可能引入或掩盖错误。

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

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

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

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

            【讨论】:

            • 这是示例中使用的模拟框架的工件。 NUnit.Mocks 不像其他模拟框架那样通用。每个模拟都是独立创建、配置和验证的。我已经切换到使用容器进行创建和验证的容器,只留下每个模拟的配置单独处理。
            【解决方案7】:

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

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

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

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

            【讨论】:

            • 如果您希望 A+B 由不同的团队同时开发(B 开发人员需要一个 mock A),那么 Mocks 会很有用。如果 A 组件真的很慢,你会很高兴在开发 B 时模拟 A。(话虽如此,在实践中我确实发现自己在做 A,然后 A+B 测试你在绝大多数情况下描述)。
            • Mocks 是一种很好且简单的测试间接输出的方法,这些输出很重要且不安全,无法忽略。
            • 使用模拟来隔离被测类的能力似乎是正确设计的标志,而不是不正确的设计。当我可以使用 jMock 时,我对我的系统设计比以前使用分层的、紧密绑定到数据库嵌套层方法的方法要舒服得多。
            • BDD 提倡由外而内的开发,因此 AccountService 通常不会在您第一次需要它时存在。 +1 对 Oni 的评论。
            猜你喜欢
            • 2012-09-06
            • 2011-08-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-01-13
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多