Авто-мок контейнер от Mark Seemann

Posted by PDSW on Tuesday, September 18, 2018
Last Modified on Monday, February 11, 2019

TOC

Как можно отделить юнит тесты от механизма Dependency Injection? В этой статье описывается паттерн юнит тестов (unit test), называемый автомок (Auto-mocking Container). Его можно использовать для устранения проблемы хрупких тестов (Fragile Test), которые так часто происходят при создании тестируемой системы (system under tests - SUT) при помощи антипаттерна Мать объекта. На этот вопрос отвечает Mark Seemann.

Предисловие переводчика: очередной перевод на этот раз статьи от Mark Seemann о авто-мок контейнерах. Несмотря на то что мне приходится работать с юнит тестами каждый день о концепции Auto mock-ов я узнал не так и двано. Что-бы зделать как можно более доступной информацию о этом полезном подходе к созданию тестовых двойников без лишней боли. И как обычно ссылка на оригинал

макет кактуса
макет кактуса

В этой статье описывается паттерн юнит тестов, называемый автомок (Auto-mocking Container). Его можно использовать для решение следующей проблемы:

Как можно отделить юнит тесты от механизма Dependency Injection ?

Это можно сделать при помощи механизма эвристической композиции при создании динамических тестовых двойников внутри тестируемой системы (system under tests - SUT).

Основная проблема юнит тестов - необходимость проверять, что они не сломались при изменениях системы. Одной из наиболее распространенных проблем программистов с юнит тестами является так называемый запах хрупких тестов (Fragile Test). Каждый раз, когда вы пытаетесь реорганизовать свой код, тесты почти полностью переделываются.

Существуют различные причины, по которым это происходит, и паттерн автомок не может помочь во всех случаях, но одной из распространенных причин, из-за которой нужно переделать тесты, в случае изменения конструктора вашей системы под тестами (SUT). В таких случаях использование автомока может помочь.

#Как aвто-мок контейнер работает?

Чтобы отделить модульный тест от механизма создания экземпляра SUT, тестовый код может переиспользовать контейнер зависимости для компоновки SUT-a. Контейнер DI должен быть настраиваемым в той мере, в какой он может автоматически собирать динамических тестовых двойников (mocks/stubs) в SUT.

автомок сиквенс диаграмма

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

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

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

Когда его использовать

Используйте автомок, когда вы покрываете юнит тестами классы, которые используют DI (в частности, Injection Constructor). В полностью слабо связной системе конструкторы представляют собой детали реализации, а это значит, что вы можете изменить сигнатуру конструктора как часть рефакторинга.

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

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

Автомок не так идеально подходит для систем, которые по большей части основаны на функциональном стиле программирования - при помощи Value Object или алгоритмов потока данных, и меньше на DI.

Детали реализации

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

Чтобы расширить контейнер DI добавив возможность обслуживания динамических мок объектов, вы также должны выбрать подходящую библиотеку динамичских моков. Контейнер авто-моков - это не что иное, как «Библиотека соединения», которая связывает поведение контейнера DI с поведением библиотеки динамических моков.

Существуют некоторые проекты с открытым исходным кодом, которые предоставляют предварительно упакованные “библиотеки соедининия” (адаптеры) вместе с авто-мок контейнерами. В таком случае, Вы можете работать с хорошим контейнером DI и динамическими макетными библиотеками, прилагая минимальные усилия для создания автомоков как части инфраструктуры юнит тестов.

Мотивационный пример

Чтобы понять, как модульные тесты могут быть тесно связаны со конструирующей механикой SUT, представьте, что вы разрабатываете простой веб-сервис корзины покупок с помощью Test-Driven Development (TDD).

Чтобы упростить пример, представьте, что корзина для покупок станет сервисом CRUD, открытым через HTTP. Структура, которую вы выбрали использовать, основана на концепции контроллера, который обрабатывает входящие запросы и отвечает на них. Чтобы начать работу, вы пишете первый (ice breaker) юнит тест:

[Fact]
public void SutIsController()
{
    var sut = new BasketController();
    Assert.IsAssignableFrom<IHttpController>(sut);
}

Это простой юнит тест, в котором предлагается создать класс BasketController.

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

[Fact]
public void PostSendsCorrectEvent()
{
    var channelMock = new Mock<ICommandChannel>();
    var sut = new BasketController(channelMock.Object);
    
    var item = new BasketItemModel { ProductId = 1234, Quantity = 3 };
    sut.Post(item);
    
    var expected = item.AddToBasket();
    channelMock.Verify(c => c.Send(expected));
}

Чтобы сделать этот тест компилируемым, вам нужно добавить такой конструктор в класс BasketController:

private ICommandChannel channel;
    
public BasketController(ICommandChannel channel)
{
    this.channel = channel;
}

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

[Fact]
public void SutIsController()
{
    var channelDummy = new Mock<ICommandChannel>();
    var sut = new BasketController(channelDummy.Object);
    Assert.IsAssignableFrom<IHttpController>(sut);
}

В этом примере только один юнит тест сломался, но по мере продвижения он становится все хуже.

Довольные вашей реализацией до сих пор, вы теперь решили реализовать функцию, в которой клиент службы может получить содержимое корзины. Это поддерживается методом HTTP GET, поэтому вы пишете этот юнит тест, чтобы задействовать эту функцию:

[Fact]
public void GetReturnsCorrectResult()
{
    // Arrange            
    var readerStub = new Mock<IBasketReader>();
    var expected = new BasketModel();
    readerStub.Setup(r => r.GetBasket()).Returns(expected);
    
    var channelDummy = new Mock<ICommandChannel>().Object;
    var sut = new BasketController(channelDummy, readerStub.Object);
    
    // Act
    var response = sut.Get();
    var actual = response.Content.ReadAsAsync<BasketModel>().Result;
    
    // Assert
    Assert.Equal(expected, actual);
}

Этот тест вводит еще одну зависимость в SUT, заставляя вас изменить конструктор BasketController на это:

private ICommandChannel channel;
private IBasketReader reader;
    
public BasketController(ICommandChannel channel, IBasketReader reader)
{
    this.channel = channel;
    this.reader = reader;
}

Увы, это нарушает оба предыдущих модульных теста, и вы должны их пересмотреть и исправить, прежде чем сможете продолжить.

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

Пометки при рефакторинге

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

Если вы внимательно изучите тесты в мотивационном примере, вы заметите, что SUT создан на этапе Arrange теста. Эта фаза теста также называется фазой установки фикстур (fixture); это место, где вы ставите весь код инициализации, который требуется, прежде чем вы сможете взаимодействовать с SUT. Если быть откровенно честным, код, который входит в фазу Arrange, является просто необходимым злом. Вы должны заботиться только о фазах Act и Assert - в конце концов, эти тесты не проверяют конструкторы. Другими словами, то, что происходит на этапе Arrange, в основном случайное, так что, к сожалению, эта часть теста удерживает вас от рефакторинга. Вам нужен способ отделить тесты от сигнатуры конструктора, в то же время оставляя возможность манипулировать введенными динамическими макетными объектами.

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

Другой подход заключается в построении SUT с помощью вспомогательного метода. Однако, если SUT имеет более одной зависимости, вам может потребоваться создать много перегрузок таких вспомогательных методов, чтобы манипулировать только динамическими моками, которые могут вас интересовать в данном тестовом случае. Это, как правило, ведет к антипаттерну «Мать объекта».

На фоне этих вариантов отличной альтеранативой является подход с использованием Auto-mocking Container, чтобы отделить тесты от сигнатуры конструктора SUT.

Пример: Castle Windsor как автомок#

В этом примере мы используем Castle Windsor в качестве автомозаичного контейнера. Castle Windsor является одним из многих контейнеров DI для .NET с довольно хорошей моделью расширяемости. Его можно использовать для превращения стандартного WindsorContainer в автомок. В этом случае вы объедините его с Moq, чтобы автоматически создавать динамические макеты каждый раз, когда вам нужен экземпляр интерфейса.

Для этого требуется всего два небольших класса. Первый класс - это так называемый SubDependencyResolver, который переводит запросы на интерфейс в запрос на макет этого интерфейса:

public class AutoMoqResolver : ISubDependencyResolver
{
    private readonly IKernel kernel;
    
    public AutoMoqResolver(IKernel kernel)
    {
        this.kernel = kernel;
    }
    
    public bool CanResolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        return dependency.TargetType.IsInterface;
    }
    
    public object Resolve(
        CreationContext context,
        ISubDependencyResolver contextHandlerResolver,
        ComponentModel model,
        DependencyModel dependency)
    {
        var mockType = typeof(Mock<>).MakeGenericType(dependency.TargetType);
        return ((Mock)this.kernel.Resolve(mockType)).Object;
    }
}

Интерфейс ISubDependencyResolver - это точка расширяемости Castle Windsor. Он следует паттерну Tester-Doer, что означает, что WindsorContainer сначала вызовет метод CanResolve, а затем вызовет только метод Resolve, если возвращаемое значение из CanResolve было true.

В этой реализации вы возвращаете true тогда и только тогда, когда запрошенная зависимость является интерфейсом. Когда это так, вы создаете общий тип класса Mock. Например, в приведенном выше коде, если dependency.TargetType - это интерфейс ICommandChannel, переменная mockType становится типом Mock.

Следующая строка кода просит ядро разрешить зависимость этого типа (например, Mock). Соответствующий экземпляр передается в Mock для доступа и возврата значения его свойства Object. Если вы спросите ядро ​​о экземпляре Mock, он вернет экземпляр этого типа, а его свойство Object будет экземпляром ICommandChannel. Это и есть динамический мок, который вам нужен, чтобы собрать SUT из автоматических тестовых дубликатов.

Другой класс собирает все вместе:

public class ShopFixture : IWindsorInstaller
{
    public void Install(
        IWindsorContainer container,
        IConfigurationStore store)
    {
        container.Kernel.Resolver.AddSubResolver(
            new AutoMoqResolver(
                container.Kernel));
        container.Register(Component
            .For(typeof(Mock<>)));
    
        container.Register(Classes
            .FromAssemblyContaining<Shop.MvcApplication>()
            .Pick()
            .WithServiceSelf()
            .LifestyleTransient());
    }
}

Это IWindsorInstaller, который является простым способом упаковки конфигураций Castle Windsor вместе. В первой строке вы добавляете AutoMoqResolver. В следующей строке вы регистрируете открытый обобщённый тип Mock <>. Это означает, что WindsorContainer знает о любых общих вариантах Mock , которые вы можете создать. Напомним, что AutoMockResolver просит ядро ​​разрешить зависимость для экземпляра Mock (например, Mock). Такая регистрация делает это возможным.

Наконец, ShopFixture просматривает все публичные типы в узле SUT и регистрирует их конкретные типы. Это все, что нужно, чтобы превратить Castle Windsor в автомок.

Как только вы это сделаете, вы можете начать TDD, и вам редко (если когда-либо) придётся возвращаться к ShopFixture, чтобы настроить что-либо.

Первый тест, который вы пишете, эквивалентен первому тесту в предыдущем мотивирующем примере:

[Fact]
public void SutIsController()
{
    var container = new WindsorContainer().Install(new ShopFixture());
    var sut = container.Resolve<BasketController>();
    Assert.IsAssignableFrom<IHttpController>(sut);
}

Сначала вы создаете контейнер и устанавливаете ShopFixture. Это будет первая строка кода во всех ваших тестах.

Затем вы попросите контейнер разрешить экземпляр BasketController. На данный момент есть только конструктор по умолчанию, поэтому автомок еще даже не вызывался, но вы получаете экземпляр SUT, на базе которого вы можете сделать свой ассёрт.

Теперь пришло время написать следующий тест, эквивалентный второму тесту в мотивирующем примере:

[Fact]
public void PostSendsCorrectEvent()
{
    var container = new WindsorContainer().Install(new ShopFixture());
    var sut = container.Resolve<BasketController>();
    
    var item = new BasketItemModel { ProductId = 1234, Quantity = 3 };
    sut.Post(item);
    
    var expected = item.AddToBasket();
    container
        .Resolve<Mock<ICommandChannel>>()
        .Verify(c => c.Send(expected));
}

Здесь происходят интересные вещи. Обратите внимание, что первые две строки кода такие же, как в предыдущем тесте. Перед созданием SUT вам не нужно определять макет объекта. Фактически, вы можете использовать SUT, не ссылаясь на макет.

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

Это возможно, потому что ShopFixture регистрирует все экземпляры Mock с так называемым стилем жизни (lifestyle) Singleton. Стиль Singleton не следует путать с шаблоном проектирования Singleton. Это означает, что каждый раз, когда вы запрашиваете экземпляр контейнера для экземпляра типа, вы получите тот же экземпляр. В Castle Windsor это стиль жизни по умолчанию, поэтому этот код:

container.Register(Component
    .For(typeof(Mock<>)));

эквивалентно этому:

container.Register(Component
    .For(typeof(Mock<>))
    .LifestyleSingleton());

Когда вы реализуете метод Post на BasketController, введя ICommandChannel в свой конструктор, тест пройдет. Это связано с тем, что, когда вы запрашиваете контейнер для разрешения экземпляра BasketController, ему необходимо сначала разрешить экземпляр ICommandChannel.

winsdor автомок сиквенс диаграмма

Это делается с помощью AutoMoqResolver, который, в свою очередь, запрашивает ядро ​​для создания экземпляра Mock. Свойство Object - это динамически созданный экземпляр ICommandChannel, который вводится в экземпляр BasketController, который в конечном итоге и возвращается контейнером.

Когда тестовый пример впоследствии запрашивает контейнер для экземпляра Mock, тот же экземпляр повторно используется, потому что он настроен с использованием стиля Singleton.

Что еще более важно, в то время как второй тест побудил вас изменить сигнатуру конструктора BasketController, это изменение не нарушило существующий тест.

Теперь вы можете написать третий тест, эквивалентный третьему тесту в мотивационном примере:

[Fact]
public void GetReturnsCorrectResult()
{
    var container = new WindsorContainer().Install(new ShopFixture());
    var sut = container.Resolve<BasketController>();
    var expected = new BasketModel();
    container.Resolve<Mock<IBasketReader>>()
        .Setup(r => r.GetBasket())
        .Returns(expected);            
    
    var response = sut.Get();
    var actual = response.Content.ReadAsAsync<BasketModel>().Result;
    
    Assert.Equal(expected, actual);
}

Еще раз вы меняете конструктор BasketController, на этот раз для ввода в него IBasketReader, но ни один из существующих тестов не прерывается.

В этом примере показано, как пересортировать Castle Windsor в качестве Auto-mocking Container. Важно понимать, что это все еще чисто забота о проблемах юнит тестов. Такой подход не требует, чтобы вы использовали Castle Windsor в своем продакшен коде.

Другие контейнеры DI так-же могут быть переиспользованы как Auto-mocking Containers. Одним из вариантов является Autofac, и, хотя он не является строго контейнером DI, другой опцией является AutoFixture.


comments powered by Disqus