这是一个糟糕的设计吗?

发布于 2024-07-24 05:02:35 字数 1774 浏览 3 评论 0原文

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

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

I'm trying my hand at behavior driven development and I'm finding myself second guessing my design as I'm writing it. This is my first greenfield project and it may just be my lack of experience. Anyway, here's a simple spec for the class(s) I'm writing. It's written in NUnit in a BDD style instead of using a dedicated behavior driven framework. This is because the project targets .NET 2.0 and all of the BDD frameworks seem to have embraced .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();
    }
}

None of the interfaces used by MainPresenter have any real implementations yet. AccountService will be responsible for creating new accounts. There can be multiple implementations of IAccount defined as separate plugins. At runtime, if there is more than one then the user will be prompted to choose which account type to create. Otherwise AccountService will simply create an account.

One of the things that has me uneasy is how many mocks are required just to write a single spec/test. Is this just a side effect of using BDD or am I going about this thing the wrong way?

[Update]

Here's the current implementation of MainPresenter.AddAccount

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

Any tips, suggestions or alternatives welcome.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

熊抱啵儿 2024-07-31 05:02:35

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

when_adding_an_account
   should_use_account_service_to_create_new_account
   should_update_screen_with_new_account_details

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

其他一些小建议...

  • 您可能需要考虑使用模拟框架,例如 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);
     }

When doing top to down development it's quite common to find yourself using a lot of mocks. The pieces you need aren't there so naturally you need to mock them. With that said this does feel like an acceptance level test. In my experience BDD or Context/Specification starts to get a bit weird at the unit test level. At the unit test level I'd probably be doing something more along the lines of...

when_adding_an_account
   should_use_account_service_to_create_new_account
   should_update_screen_with_new_account_details

You may want to reconsider your usage of an interface for IAccount. I personally stick
with keeping interfaces for services over domain entities. But that's more of a personal preference.

A few other small suggestions...

  • You may want to consider using a Mocking framework such as Rhino Mocks (or Moq) which allow you to avoid using strings for your assertions.
  _mockAccountService.Expect(mock => mock.Create())
     .Return(_account);

  • If you are doing BDD style one common pattern I've seen is using chained classes for test setup. In your example...
public class MainPresenterSpec 
{
    // Protected variables for Mocks 

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

}

[TestFixture]
public class WhenUserAddsAccount : MainPresenterSpec
{
    [Test]
    public void ShouldCreateNewAccount()
    {
    }
}
  • Also I'd recommend changing your code to use a guard clause..
     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);
     }
情绪 2024-07-31 05:02:35

如果您使用自动模拟容器,例如 RhinoAutoMocker(结构图)。 您使用自动模拟容器创建被测试的类,并向其询问测试所需的依赖项。 容器可能需要在构造函数中注入 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

The test life support is a lot simpler if you use an auto mocking container such as RhinoAutoMocker (part of StructureMap) . You use the auto mocking container to create the class under test and ask it for the dependencies you need for the test(s). The container might need to inject 20 things in the constructor but if you only need to test one you only have to ask for that one.

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();
            }
        }
    }
}

Structurally I prefer to use underscores between words instead of RunningThemAllTogether as I find it easier to scan. I also create an outer class named for the class under test and multiple inner classes named for the method under test. The test methods then allow you to specify the behaviors of the method under test. When run in NUnit this gives you a context like:

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
绝影如岚 2024-07-31 05:02:35

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

不过,这看起来更像是验收测试而不是单元测试 - 也许如果您降低断言复杂性,您会发现更少的关注点被嘲笑。

That seems like the correct number of mocks for a presenter with a service which is supposed to hand back an account.

This seems more like an acceptance test rather than a unit test, though - perhaps if you reduced your assertion complexity you would find a smaller set of concerns being mocked.

情话墙 2024-07-31 05:02:35

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

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

这就像海森堡不确定性原理 - 一旦你有了模拟,你的代码就会发生很大的变化,这会成为维护的难题,而且模拟本身有可能引入或掩盖错误。

如果您有干净的接口,那么我对实现一个模拟(或模拟)另一个模块的未实现接口的简单接口没有异议。 这种模拟可以像模拟一样用于单元测试等。

Yes, your design is flawed. You are using mocks :)

More seriously, I agree with the previous poster who suggests your design should be layered, so that each layer can be tested separately. I think it is wrong in principle that testing code should alter the actual production code -- unless this can be done automatically and transparently the way code can be compiled for debug or release.

It's like the Heisenberg uncertainty principle - once you have the mocks in there, your code is so altered it becomes a maintenance headache and the mocks themselves have the potential to introduce or mask bugs.

If you have clean interfaces, I have no quarrel with implementing a simple interface that simulates (or mocks) an unimplemented interface to another module. This simulation could be used in the same way mocking is, for unit testing etc.

爱殇璃 2024-07-31 05:02:35

You might want to use MockContainers in order to get rid of all the mock management, while creating the presenter. It simplifies unit tests a lot.

寄人书 2024-07-31 05:02:35

这没关系,但我希望那里有一个 IoC 自动模拟容器。 代码暗示测试编写者手动(明确)在测试中的模拟对象和真实对象之间切换,但事实不应该是这样,因为如果我们谈论的是单元测试(单元只是一个类),自动模拟所有其他类并使用模拟会更简单。

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

This is okay, but I would expect an IoC automocking container in there somewhere. The code hints at the test writer manually (explicitly) switching between mocked and real objects in tests which should not be the case because if we are talking about a unit test (with unit being just one class), it's simpler to just auto-mock all other classes and use the mocks.

What I'm trying to say is that if you have a test class that uses both mainView and mockMainView, you don't have a unit test in the strict sense of the word -- more like an integration test.

彡翼 2024-07-31 05:02:35

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

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

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

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

It is my opinion that if you find yourself needing mocks, your design is incorrect.

Components should be layered. You build and test components A in isolation. Then you build and test B+A. Once happy, you build layer C and test C+B+A.

In your case you shouldn't need a "_mockAccountService". If your real AccountService has been tested, then just use it. That way you know any bugs are in MainPresentor and not in the mock itself.

If your real AccountService hasn't been tested, stop. Go back and do what you need to ensure it is working correctly. Get it to the point where you can really depend on it, then you won't need the mock.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文