Дизайн по контракту, написание тестового кода, построение объекта и зависимость от инъекций, объединяющие лучшие практики

Я пытаюсь выяснить лучшие практики написания тестового кода, но более конкретно методы, связанные с построением объектов. В синей книге мы обнаружили, что мы должны применять инварианты при создании объектов, чтобы избежать искажения наших сущностей, объектов ценности и т.д. С учетом этой мысли, 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, но это то, с чем я могу жить для своих приложений. При разработке многоразовой рамки все, конечно, отличается. В этом случае вам, вероятно, понадобятся все нулевые проверки.