Проектирование по контракту, написание кода, удобного для тестирования, конструирование объектов и внедрение зависимостей, объединение лучших практик - PullRequest
5 голосов
/ 14 марта 2012

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

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

Миско предлагает, чтобы мы не злоупотребляли нулевыми проверками в коде (что противоречит Design By Contract), потому что при этом написание тестов становится проблемой, в качестве альтернативы он говорит, что лучше писать больше тестов, чем "просто иллюзия, что наш код безопасен только потому, что у нас везде есть нулевые проверки "

Что вы думаете об этом? Как бы вы это сделали? Какой должна быть лучшая практика?

Ответы [ 5 ]

6 голосов
/ 14 марта 2012

Мне нужно создать макет заглушки или фиктивный объект для каждой зависимости

Это обычно утверждается.Но я думаю, что это неправильно.Если Car связан с объектом Engine, почему бы не использовать объект real Engine при модульном тестировании вашего Car класса?

Но кто-то объявит:если вы делаете это, вы не unit тестируете свой код;ваш тест зависит как от класса Car, так и от класса Engine: два модуля, так что интеграционный тест, а не модульный тест.Но эти люди тоже издеваются над классом String?Или HashSet<String>?Конечно, нет.Граница между юнит-тестированием и интеграционным тестированием не столь ясна.

Если говорить более философски, во многих случаях вы не можете создавать хорошие фиктивные объекты.Причина в том, что для большинства методов способ делегирования объекта связанным объектам равен undefined .Будет ли это делегировано, и как, оставлено контрактом в качестве детали реализации.Единственное требование заключается в том, чтобы при делегировании метод удовлетворял предварительным условиям своего делегата.В такой ситуации подойдет только полностью функциональный (не фиктивный) делегат.Если реальный объект проверяет свои предварительные условия, невыполнение предварительного условия при делегировании вызовет сбой теста.И отладка этого теста будет легкой.

4 голосов
/ 14 марта 2012

Взгляните на концепцию построителей тестовых данных .

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

Или вы можете взглянуть на источники Enterprise Library .Тесты содержат базовый класс ArrangeActAssert, который обеспечивает хорошую поддержку для тестов BDD-ish.Вы реализуете свою настройку теста в методе Arrange класса, производного от AAA, и он будет вызываться всякий раз, когда вы запускаете конкретный тест.

1 голос
/ 14 марта 2012

Вы должны рассмотреть инструменты, чтобы сделать такую ​​работу за вас.Как AutoFixture .По сути, это создает объекты.Как бы просто это не звучало, AutoFixture может делать именно то, что вам нужно - создание объекта с некоторыми параметрами, которые вас не интересуют :

MyClass sut = fixture.CreateAnnonymous<MyClass>();

MyClass будет создано с фиктивными значениями для параметров конструктора, свойств и т. Д. (Обратите внимание, что это будут не значения по умолчанию, такие как null, а фактические экземпляры - все же это сводится к одному и тому же; поддельные нерелевантные значения, которыенужно быть там).

Редактировать: Чтобы немного расширить введение ...

AutoFixure также поставляется с расширением AutoMoq, чтобы стать полноценным авто-насмешливый контейнер .Когда AutoFixture не может создать объект (а именно, интерфейс или абстрактный класс), он делегирует создание в Moq - вместо этого создаются mocks.

Итак, если у вас был класс с сигнатурой конструктора, такой как:

public ComplexType(IDependency d, ICollaborator c, IProvider p)

Ваша тестовая установка в сценарии, когда вы не заботитесь о каких-либо зависимостях и хотите просто nulls, будет состоять целиком из 2 строк кода:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var testedClass = fixture.CreateAnonymous<ComplexType>();

Это все, что есть.testedClass будет создан с помощью макетов, сгенерированных Moq под капотом.Обратите внимание, что testedClass это не макет - это реальный объект, который вы можете проверить, как если бы вы создали его с помощью конструктора.

Он становится еще лучше.Что делать, если вы хотите, чтобы некоторые макеты создавались динамически с помощью AutoFixture-Moq, но некоторые другие макеты вы хотели бы иметь больше контроля, например.проверить в данном тесте?Все, что вам нужно, это одна дополнительная строка кода :

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var collaboratorMock = fixture.Freeze<Mock<ICollaborator>>();
var testedClass = fixture.CreateAnonymous<ComplexType>();

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

1 голос
/ 14 марта 2012

Я решаю эту проблему в модульных тестах:

Мой тестовый класс для автомобиля будет выглядеть следующим образом:

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
}
0 голосов
/ 14 марта 2012

Я знаю, что не все согласны со мной (я знаю, что Марк Симанн не согласится со мной), но я, как правило, не делаю нулевых проверок в моих конструкторах для типов, которые создаются контейнером с использованием инжекции конструктора.По двум причинам, во-первых, это (иногда) усложняет тестирование - как вы уже заметили -.Но кроме того, это просто добавляет больше шума в код.Все DI-контейнеры (о которых я знаю) не позволяют вставлять нулевые ссылки в конструкторы, поэтому мне не нужно усложнять мой код для чего-то, что не произойдет в любом случае.

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

...