Это плохой дизайн? - PullRequest
5 голосов
/ 10 июня 2009

Я пробую свои силы в разработке, основанной на поведении, и нахожу себя вторым, угадывающим мой дизайн, когда я пишу его. Это мой первый проект с нуля, и это может быть просто отсутствие опыта. В любом случае, вот простая спецификация для класса (классов), который я пишу. Он написан на NUnit в стиле BDD вместо использования специализированной структуры, управляемой поведением. Это связано с тем, что проект нацелен на .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 или я поступаю неправильно?

[Update]

Вот текущая реализация MainPresenter.AddAccount

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

Любые советы, предложения или альтернативы приветствуются.

Ответы [ 7 ]

3 голосов
/ 10 июня 2009

При разработке сверху вниз довольно часто приходится использовать множество насмешек. Куски, в которых вы нуждаетесь, отсутствуют, поэтому, естественно, вам нужно издеваться над ними. С учетом сказанного это похоже на приемочный тест. По моему опыту, BDD или Context / Specification начинают становиться немного странными на уровне модульного теста. На уровне юнит-теста я, вероятно, буду делать что-то более похожее на ...

when_adding_an_account
   should_use_account_service_to_create_new_account
   should_update_screen_with_new_account_details

Возможно, вы захотите пересмотреть использование интерфейса для IAccount. Я лично придерживаюсь с сохранением интерфейсов для услуг над объектами домена. Но это больше личного предпочтения.

Несколько других небольших предложений ...

  • Вы можете рассмотреть возможность использования Mocking Framework, такого как 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);
     }
2 голосов
/ 11 июня 2009

Поддержка жизненного цикла теста намного проще, если вы используете контейнер для автоматической проверки, например 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();
            }
        }
    }
}

Конструктивно я предпочитаю использовать подчеркивание между словами вместо RunningThemAllTo Вместе, так как мне легче сканировать. Я также создаю внешний класс, названный для тестируемого класса, и несколько внутренних классов, названных для тестируемого метода. Методы тестирования позволяют затем определить поведение тестируемого метода. При запуске в 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 голос
/ 11 июня 2009

Да, ваш дизайн имеет недостатки. Вы используете mocks:)

Более серьезно, я согласен с предыдущим постером, который предлагает, чтобы ваш дизайн был многослойным, чтобы каждый слой можно было тестировать отдельно. Я думаю, что в принципе неправильно, что тестируемый код должен изменять реальный производственный код - если это не может быть сделано автоматически и прозрачно, как код может быть скомпилирован для отладки или выпуска.

Это похоже на принцип неопределенности Гейзенберга - когда у вас есть макеты, ваш код настолько изменен, что становится головной болью при обслуживании, и сами макеты могут потенциально представлять или маскировать ошибки.

Если у вас чистые интерфейсы, я не ссорюсь с реализацией простого интерфейса, который имитирует (или имитирует) нереализованный интерфейс с другим модулем. Эту симуляцию можно использовать таким же образом, как и для насмешки, для модульного тестирования и т. Д.

1 голос
/ 10 июня 2009

Похоже на правильное количество издевательств для докладчика с услугой, которая должна вернуть счет.

Это больше похоже на приемочный тест, а не на модульный тест, хотя - возможно, если вы уменьшите сложность вашего утверждения, вы обнаружите меньший набор проблем, которые высмеивают.

0 голосов
/ 11 июня 2009

По моему мнению, если вам нужны насмешки, ваш дизайн неверен.

Компоненты должны быть слоистыми. Вы строите и тестируете компоненты A изолированно. Затем вы строите и тестируете B + A. Когда вы счастливы, вы создаете слой C и тестируете C + B + A.

В вашем случае вам не нужен "_mockAccountService". Если ваш реальный AccountService был протестирован, просто используйте его. Таким образом, вы знаете, что любые ошибки есть в MainPresentor, а не в самом макете.

Если ваш реальный AccountService не был протестирован, остановитесь. Вернитесь и сделайте то, что вам нужно, чтобы убедиться, что он работает правильно. Доведите это до такой степени, что вы действительно можете зависеть от этого, тогда вам не понадобится издеваться.

0 голосов
/ 10 июня 2009

Это нормально, но я бы ожидал, что где-нибудь там будет контейнер для автоматической блокировки IoC. Код указывает на то, что создатель тестов вручную (явно) переключается между проверенными и реальными объектами в тестах, чего не должно быть, потому что, если мы говорим о тесте unit (с единицей, являющейся всего одним классом), это проще просто смоделировать все другие классы и использовать макеты.

Я пытаюсь сказать, что если у вас есть тестовый класс, который использует как mainView, так и mockMainView, у вас нет модульного теста в строгом смысле этого слова - больше похоже на интеграцию тест.

0 голосов
/ 10 июня 2009

Возможно, вы захотите использовать MockContainers , чтобы избавиться от всего управления имитацией при создании докладчика. Это значительно упрощает юнит-тесты.

...