Дизайн по контракту, написание тестового кода, построение объекта и зависимость от инъекций, объединяющие лучшие практики
Я пытаюсь выяснить лучшие практики написания тестового кода, но более конкретно методы, связанные с построением объектов. В синей книге мы обнаружили, что мы должны применять инварианты при создании объектов, чтобы избежать искажения наших сущностей, объектов ценности и т.д. С учетом этой мысли, Design By Contract кажется решением, чтобы избежать повреждения наших объектов, но когда мы следуем этому, мы могли бы написать код следующим образом:
class Car
{
//Constructor
public Car(Door door, Engine engine, Wheel wheel)
{
Contract.Requires(door).IsNotNull("Door is required");
Contract.Requires(engine).IsNotNull("Engine is required");
Contract.Requires(wheel).IsNotNull("Wheel is required");
....
}
...
public void StartEngine()
{
this.engine.Start();
}
}
Хорошо, это хорошо выглядит на первый взгляд? Кажется, мы строим безопасный класс, который раскрывает контракт, поэтому каждый раз, когда создается объект Car
, мы можем точно знать, что объект "действителен".
Теперь рассмотрим этот пример с точки зрения тестирования.
Я хочу создать тестовый код, но для того, чтобы изолировать мой объект Car
, мне нужно создать либо макет заглушки, либо фиктивный объект для каждой зависимости только для создания моего объекта, даже если возможно, я просто хочу протестировать метод, который использует только одну из этих зависимостей, например метод StartEngine
. Следуя философии тестирования Misko Hevery, я бы хотел написать свой тест, явно указав, что мне не нужны объекты Door или Wheel, просто передающие нулевую ссылку на конструктор, но поскольку я проверяю нули, я просто не могу этого сделать
Это всего лишь небольшая часть кода, но когда вы сталкиваетесь с реальным тестированием приложений, становится все труднее и труднее, потому что вам нужно разрешать зависимости для вашего объекта
Misko предлагает, чтобы мы не злоупотребляли нулевыми чеками в коде (что противоречит дизайну по контракту) из-за этого, писать тесты становится болью, в качестве альтернативы он советует лучше писать больше тестов, чем "просто что наш код безопасен только потому, что у нас есть нулевые проверки везде"
Что вы думаете об этом?
Как бы вы это сделали?
Какая должна быть лучшая практика?
Ответы
Ответ 1
Посмотрите на концепцию тестовых сборщиков данных.
Создайте построитель один раз с предварительно сконфигурированными данными, переопределите свойство, если необходимо, и вызовите Build()
, чтобы получить новый экземпляр тестируемой системы.
Или вы можете взглянуть на источники Enterprise Library. Тесты содержат базовый класс ArrangeActAssert
, который обеспечивает хорошую поддержку тестов BDD-ish. Вы реализуете свою тестовую настройку в методе Arrange
класса, производного от AAA, и он будет вызываться всякий раз, когда вы запускаете определенный тест.
Ответ 2
Мне нужно создать либо макет заглушки, либо фиктивный объект для каждой зависимости
Об этом обычно говорится. Но я думаю, что это неправильно. Если a Car
связано с объектом Engine
, почему бы не использовать реальный объект Engine
при модульном тестировании вашего класса Car
?
Но кто-то объявит, если вы это сделаете, вы не будете тестировать ваш код; ваш тест зависит как от класса Car
, так и от класса Engine
: два блока, поэтому вместо теста unit test выполняется интеграционный тест. Но эти люди тоже издеваются над классом String
? Или HashSet<String>
? Конечно нет. Линия между модулем и интеграционным тестированием не так понятна.
Более философски, во многих случаях вы не можете создавать хорошие макеты. Причина в том, что для большинства методов способ, которым объект делегирует связанные объекты, undefined. Независимо от того, делегирует ли он и каким образом, договор остается в качестве детали реализации. Единственное требование состоит в том, что при делегировании метод удовлетворяет предварительным условиям его делегата. В такой ситуации будет выполняться только полнофункциональный (немощный) делегат. Если реальный объект проверяет свои предварительные условия, неспособность выполнить предварительное условие при делегировании приведет к сбою теста. И отладка этого теста будет легкой.
Ответ 3
Я решаю эту проблему в модульных тестах:
Мой тестовый класс для автомобиля будет выглядеть следующим образом:
public sealed class CarTest
{
public Door Door { get; set; }
public Engine Engine { get; set; }
public Wheel Wheel { get; set; }
//...
[SetUp]
public void Setup()
{
this.Door = MockRepository.GenerateStub<Door>();
//...
}
private Car Create()
{
return new Car(this.Door, this.Engine, this.Wheel);
}
}
Теперь, в методах тестирования, мне нужно указать только "интересные" объекты:
public void SomeTestUsingDoors()
{
this.Door = MockRepository.GenerateMock<Door>();
//... - setup door
var car = this.Create();
//... - do testing
}
Ответ 4
Вам следует подумать о инструментах для выполнения этой работы. Как AutoFixture. По существу, он создает объекты. Как бы просто это ни звучало, AutoFixture может делать именно то, что вам нужно здесь - создать объект с некоторыми параметрами, которые вам не нужны:
MyClass sut = fixture.CreateAnnonymous<MyClass>();
MyClass
будет создан с фиктивными значениями для параметров, свойств конструктора и т.д. (обратите внимание, что они не будут значениями по умолчанию, такими как null
, но фактическими экземплярами - все же это сводится к одному и тому же: нерелевантные значения, которые должны быть там).
Изменить: Чтобы немного расширить введение...
AutoFixure также поставляется с расширением AutoMoq, чтобы стать полноэкранным самосознающим контейнером. Когда AutoFixture не может создать объект (а именно интерфейс или абстрактный класс), он делегирует создание Moq, который вместо этого создает mocks.
Итак, если у вас был класс с такой конструкторской сигнатурой, как это:
public ComplexType(IDependency d, ICollaborator c, IProvider p)
Ваша тестовая установка в сценарии, когда вы не заботитесь о каких-либо зависимостях и хотите просто nulls
, будет состоять полностью из двух строк кода:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var testedClass = fixture.CreateAnonymous<ComplexType>();
Это все, что есть. testedClass
будет создан с помощью mocks, созданных Moq под капотом. Обратите внимание, что testedClass
не является mock - это реальный объект, который вы можете протестировать так же, как если бы вы его создали с помощью конструктора.
Это еще лучше. Что делать, если вы хотите, чтобы некоторые макеты создавались динамически с помощью AutoFixture-Moq, но некоторые другие издевательства, над которыми вы хотите получить больше контроля, например. проверить в данном тесте? Все, что вам нужно, это одна дополнительная строка кода:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var collaboratorMock = fixture.Freeze<Mock<ICollaborator>>();
var testedClass = fixture.CreateAnonymous<ComplexType>();
ICollaborator
будет высмеять, что вы получили полный доступ, можете сделать .Setup
, .Verify
и все связанные вещи. Я действительно предлагаю взглянуть на AutoFixture - это отличная библиотека.
Ответ 5
Я знаю, что не все со мной согласны (я знаю, что Марк Сееман не согласен со мной), но я вообще не делаю нулевых проверок в своих конструкторах для типов, созданных контейнером, с использованием инъекции конструктора. По двум причинам, прежде всего, это (иногда) усложняет тестирование, как вы уже заметили. Но кроме того, он просто добавляет больше шума в код. Все контейнеры DI (которые я знаю) не позволят вводить нулевые ссылки в конструкторы, поэтому мне не нужно усложнять мой код тем, что не произойдет в любом случае.
Конечно, вы можете утверждать, что, поскольку я оставляю нулевые проверки для моих типов сервисов, эти типы теперь неявно знают о существовании контейнера DI, но это то, с чем я могу жить для своих приложений. При разработке многоразовой рамки все, конечно, отличается. В этом случае вам, вероятно, понадобятся все нулевые проверки.