Объект модульного тестирования, время жизни которого обрабатывается контейнером IoC

Я использую модульный тест Microsoft и имею следующее:

public class AccountCommandHandlers :
    Handler<CreateAccountCommand>,
     Handler<CloseAccountCommand>
{
    public bool CreateAccountCommandWasCalled = false;
    public bool CloseAccountCommandWasCalled = false;

    public void Handle(CreateAccountCommand command)
    {
        CreateAccountCommandWasCalled = true;
    }

    public void Handle(CloseAccountCommand command)
    {
        CloseAccountCommandWasCalled = true;
    }
}

[TestMethod]
public void CanRaiseInternalHandlers()
{
    var iocContainer = SimpleInjectorWiringForMembus.Instance;
    iocContainer.Bootstrap(
        AppDomain.CurrentDomain.GetAssemblies());

    var membus = MembusWiring.Instance;
    membus.Bootstrap();

    membus.Bus.Publish(new CreateAccountCommand() { Id = 100 });
    membus.Bus.Publish(new CloseAccountCommand() { Id = 100 });
}

Я использую контейнер IoC (Simple Injector), который обрабатывает объем жизни объектов. Membus связывает команды с обработчиками команд и разрешается через контейнер IoC.

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

Однако, поскольку Simple Injector обрабатывает область действия на весь срок службы, я не могу попросить Simple Injector для AccountCommandHandler объект, как он будет возвращать новый объект с CreateAccountCommandWasCalled установить в ложь.

Будучи новичком в модульном тестировании, что может быть более надежным способом тестирования, чем настройка CreateAccountCommandWasCalled как статическая переменная?

4 ответа

Решение

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

Однако с интеграционными тестами вы не должны использовать фиктивные или тупые объекты. У издевательств и окурков есть свое применение в модульном тестировании. Модульный тест - это тестирование наименьшей возможной части вашего кода. В модульном тесте вы используете mocks для управления поведением всех зависимостей вашего класса. Год назад я написал блог, в котором рассказывается о различиях между интеграцией и модульными тестами и о том, как использовать макеты в своих тестах.

В вашей ситуации я бы не использовал контейнер IoC с производственной конфигурацией для настройки ваших модульных тестов. Вместо этого я бы переключился на ручное создание ваших объектов в ваших тестах и ​​использование инструмента-насмешки, такого как Moq, для управления зависимостями.

Но это также то, что может быть автоматизировано. Отличным инструментом является AutoFixture. "Приспособление" относится к базовой линии, необходимой для выполнения ваших тестов. Это могут быть некоторые примеры данных, макеты и заглушки, которые вам нужны, и другой код установки.

Марк Симанн (Mark Seemann) (разработчик AutoFixture) пару недель назад написал замечательный блог об использовании AutoFixture вместе с IoC в качестве контейнера для Auto-Mocking. Я бы посоветовал использовать что-то подобное для структурирования ваших юнит-тестов.

Вот более "философский" ответ на ваш вопрос:-)

Я бы порекомендовал не использовать контейнер IOC при тестировании, если это возможно!

Мое обоснование заключается в том, что вам нужно, чтобы ваш тест имел полный контроль над контекстом теста, и МОК может отобрать часть этого контроля. ИМО, юнит-тесты должны быть максимально сфокусированными, небольшими и предсказуемыми!

Подумайте об отправке фиктивных объектов в тестируемый класс вместо реальных классов.

Если вашему классу нужен внутренний экземпляр контейнера IOC, выделите его из класса в "контроллер" некоторых видов.

Вы могли бы сделать это несколькими способами, мое любимое использование фреймворка, такого как Rhino Mocks.

Таким образом, вы фактически остановите "жизненный цикл", предоставленный IOC во время выполнения, в своей тестовой "настройке".

Таким образом, тест должен иметь полный контроль (посредством насмешек и создания заглушек) над тем, когда объекты создаются или уничтожаются, используя такую ​​среду, как Rhino.

Вы могли бы издеваться над МОК, если это даже нужно.

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

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

Обратите внимание, что обычно также важно получить четкое представление о том, что вы на самом деле тестируете. Модульные тесты, как правило, должны быть направлены на тестирование поведения одного метода в одном классе.

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

Еще одно замечание: я не претендую на звание эксперта по юнит-тестированию! Они невероятно полезны, но я все еще борюсь с тестированием больше, чем с любым другим аспектом кодирования.

Для дальнейшего чтения я настоятельно рекомендую "Искусство модульного тестирования" Роя Ошерова. Есть и другие тоже.

Искусство юнит-тестирования

Как сказал Стивен в своих комментариях, похоже, что вы пишете интеграционный тест, и в этом случае имеет смысл использовать контейнер IoC.

Ваш тестовый проект должен иметь свой собственный корень композиции для конфигурации IoC. Вы можете настроить свой IoC-контейнер так, чтобы он возвращал фиктивный объект для AccountCommandHandlers. Ваш тест вместо проверки логических членов может вместо этого проверить, что Handle (CreateCommand) был вызван хотя бы один раз. С Moq это будет выглядеть примерно так:

mock.Verify(foo => foo.Handle(createAccountCommand), Times.AtLeastOnce());

Если по какой-либо причине вы не можете использовать макет фреймворка с конфигурацией IoC, было бы легко создать свой собственный класс макета для этого одного теста.

Один вопрос, который вы также должны задать себе: что я на самом деле хочу проверить?

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

Если вы действительно хотите протестировать сообщение -> делегирование обработчика IOC, в случае Membus вы можете написать для тестирования адаптер шины -> IOC:

public class TestAdapter : IocAdapter
{
    private readonly object[] _handlers;

    public TestAdapter(params object[] handlers)
    {
        _handlers = handlers;
    }

    public IEnumerable<object> GetAllInstances(Type desiredType)
    {
        return _handlers;
    }
}

А затем используйте его с тестируемыми экземплярами.

        var bus =
            BusSetup.StartWith<Conservative>()
                    .Apply<IoCSupport>(
                        ioc => ioc.SetAdapter(new TestAdapter(new Handler<XYZ>()))
                    .SetHandlerInterface(typeof (IHandler<>)))
                    .Construct();

где вы бы держали экземпляр Handler, чтобы делать утверждения против него. OTOH, если вы доверяете библиотеке выполнять свою работу, вы просто обновляете обработчик и проверяете его внутреннюю логику.

Другие вопросы по тегам