这是一个糟糕的设计吗?

时间:2009-06-10 15:41:21

标签: c# unit-testing bdd

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

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

7 个答案:

答案 0 :(得分:3)

当进行自上而下的开发时,发现自己使用大量的模拟是很常见的。你需要的东西不是那么自然你需要嘲笑它们。据说这确实感觉像接受水平测试。根据我的经验,BDD或Context / Specification在单元测试级别开始有点奇怪。在单元测试级别,我可能会做更多的事情......

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风格,我见过的一个常见模式是使用链式类进行测试设置。在你的例子中......
public class MainPresenterSpec 
{
    // Protected variables for Mocks 

    [SetUp]
    public void Setup()
    {
       // Setup Mocks
    }

}

[TestFixture]
public class WhenUserAddsAccount : MainPresenterSpec
{
    [Test]
    public void ShouldCreateNewAccount()
    {
    }
}
  • 另外,我建议您更改代码以使用保护条款。
     public void AddAccount()
     {
        if (AccountService.AccountTypes.Count != 1)
        {
            // Do whatever you want here.  throw a message?
        return;
        }

    IAccount account = AccountService.Create();

        _view.Accounts.Add(account);
     }

答案 1 :(得分:2)

如果使用自动模拟容器(如RhinoAutoMocker(StructureMap的一部分)),测试生命支持会更加简单。您可以使用auto mocking容器来创建测试中的类,并询问它是否需要测试所需的依赖项。容器可能需要在构造函数中注入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

答案 2 :(得分:1)

对于有服务的演示者而言,这似乎是正确的模拟数量,应该提交一个帐户。

这似乎更像是接受测试而不是单元测试 - 也许如果你减少了断言的复杂性,你会发现一些较小的问题被嘲笑。

答案 3 :(得分:1)

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

更严重的是,我同意之前的海报,他建议您的设计应该是分层的,这样每个层都可以单独测试。我认为原则上测试代码应该改变实际的生产代码是错误的 - 除非这可以自动且透明地完成代码编译以便调试或发布的方式。

这就像海森堡的不确定性原则 - 一旦你在那里有嘲讽,你的代码就会被改变,这会让人头疼,而且嘲笑本身也有可能引入或掩盖瑕疵。

如果你有干净的接口,我没有争吵实现一个简单的接口,模拟(或模拟)到另一个模块的未实现的接口。对于单元测试等,此模拟可以与模拟相同的方式使用。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

我认为,如果你发现自己需要嘲笑,那么你的设计是错误的。

组件应该是分层的。您可以单独构建和测试组件A.然后你构建并测试B + A.一旦开心,你就构建了C层并测试了C + B + A.

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

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