TDD: заглушка, макет или ничего из вышеперечисленного - PullRequest
4 голосов
/ 26 января 2009

Я пытаюсь изучить TDD, применяя его к простому моему проекту. Некоторые детали (и более ранний вопрос) здесь:

TDD: Помощь в написании тестируемого класса

Специфика в том, что у меня есть класс PurchaseOrderCollection, у которого есть закрытый список PurchaseOrders (передан в конструктор), а у PurchaseOrders есть логическое свойство IsValid. PurchaseOrderCollection имеет свойство HasErrors, которое возвращает значение true, если для любого элемента PurchaseOrders в списке значение IsValid равно false. Это логика, которую я хочу проверить.

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{
    List<PurchaseOrder> orders = new List<PurchaseOrder>();

    orders.Add(new PurchaseOrder(--some values to generate IsValid false--));
    orders.Add(new PurchaseOrder(--some values to generate IsValid true--));

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

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

По сути, я хочу иметь возможность объявить PurchaseOrder, для которого IsValid имеет значение false или true, не зная ничего о том, что такое PurchaseOrder.

Из моего ограниченного знания TDD, это то, что вы используете заглушки или издевательства. Мой главный вопрос, это правильно? Или я должен использовать другой метод для этого? Или я совершенно испорчен и просто пишу этот тест и думаю о нем неправильно?

Первоначально я думал о том, чтобы просто использовать какую-то фиктивную инфраструктуру и создать PurchaseOrder, который всегда возвращает true или false. Из того, что я прочитал, мне нужно объявить IsValid виртуальным. Поэтому моей второй мыслью было изменить его, добавив IPurchaseOrder в качестве интерфейса для PurchaseOrder и просто создать поддельный PurchaseOrder, который всегда возвращает false или true. Являются ли обе эти действительные идеи?

Спасибо!

Ответы [ 7 ]

6 голосов
/ 26 января 2009

Вы на правильном пути, создавая заглушку или макет. Я предпочитаю использовать Mocking Framework.

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

Пример использования Moq , если вы используете C # 3.0 и .NET Framework 3.5:

[TestMethod]
public void Purchase_Order_Collection_Has_Errors_Is_True_If_Any_Purchase_Order_Has_Is_Valid_False()
{    
    var mockFirstPurchaseOrder = new Mock<IPurchaseOrder>();
    var mockSecondPurchaseOrder = new Mock<IPurchaseOrder>();

    mockFirstPurchaseOrder.Expect(p => p.IsValid).Returns(false).AtMostOnce();
    mockSecondPurchaseOrder.Expect(p => p.IsValid).Returns(true).AtMostOnce();

    List<IPurchaseOrder> purchaseOrders = new List<IPurchaseOrder>();
    purchaseOrders.Add(mockFirstPurchaseOrder.Object);
    purchaseOrders.Add(mockSecondPurchaseOrder.Object);

    PurchaseOrderCollection collection = new PurchaseOrderCollection(orders);

    Assert.IsTrue(collection.HasErrors);
}

Edit:
Здесь я использовал интерфейс для создания макета PurchaseOrder, но у вас его тоже нет. Вы можете пометить IsValid как виртуальный и смоделировать класс PurchaseOrder. Мое эмпирическое правило, когда нужно выбрать виртуальный путь. Просто создать интерфейс, чтобы я мог издеваться над объектом без какой-либо архитектурной причины, для меня запах кода.

2 голосов
/ 26 января 2009

... этот тест слишком связан в том, что я должен знать логику того, что делает PurchaseOrder IsValid false или true для пройти тест, когда действительно этот тест не должно волновать ...

Я бы на самом деле утверждал обратное - что для вашего теста знание того, что валидность моделируется как логическое значение в заказе на покупку, означает, что ваш тест слишком много знает о реализации PurchaseOrder (учитывая, что на самом деле это тест BuyOrderCollection ). У меня нет проблем с использованием реальных знаний (то есть фактических значений, которые были бы действительными или недействительными) для создания соответствующих тестовых объектов. В конечном счете, это действительно то, что вы тестируете (если я передам моей коллекции заказ на покупку со смешными ценностями, он правильно сообщит мне, что есть ошибки).

В общем, я стараюсь избегать написания интерфейса для объекта «сущность», такого как PurchaseOrder, если только для этого нет какой-либо причины, кроме как для тестирования (например, в производстве есть несколько видов PurchaseOrders, и интерфейс - лучший способ смоделировать это).

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

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

Создайте PurchaseOrderValidityChecker с интерфейсом. Используйте это при настройке логического значения isValid. Теперь создайте тестовую версию средства проверки достоверности, которая позволяет указать, какой ответ дать. (Обратите внимание, что для этого решения, вероятно, также требуется PurchaseOrderFactory или эквивалент для создания PurchaseOrders, чтобы при создании каждого заказа на покупку можно было получить ссылку на PurchaseOrderValidityChecker.)

1 голос
/ 07 сентября 2009

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

Следующий вопрос, который возникает, это что за интерфейс. Какую роль должны выполнять объекты в коллекции? Возможно, вам нужно только знать, действительны ли они, и в этом случае вы можете извлечь IValidatable и сузить зависимости в коде. Я не знаю, что верно в этом случае, но я часто нахожу, что могу использовать интерфейсы, чтобы подтолкнуть меня к более сфокусированному коду.

1 голос
/ 26 января 2009

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

издевательство над заказом на покупку ничего не выигрывает - вы проверяли макет, а не реальный класс

используя заглушку - тоже самое

все в порядке - если не обязательно - для теста белого ящика при использовании TDD

1 голос
/ 26 января 2009

Я недавно задал несколько похожий вопрос о тестировании. Не забывайте об этом: делайте самое простое, что вам нужно сделать, а затем рефакторинг при необходимости. Лично я стараюсь помнить о более широкой картине, но я также противостою желанию пересмотреть мои решения. Вы можете добавить два поля PurchaseOrder в свой тестовый класс, где одно является действительным, а другое - недействительным. Используйте эти поля, чтобы перевести ваш PurchaseOrderCollection в состояние, которое вы хотите проверить. В конечном итоге вам нужно научиться издеваться, но в этом случае вам не нужен кувалда, когда обычный молоток решит проблему. Вы не получаете никакого значения, используя фиктивный PurchaseOrder вместо конкретного PurchaseOrder, который находится в нужном вам состоянии.

Самое главное, вы получите гораздо больше от тестирования поведения PurchaseOrderCollection, а не просто от проверки состояния вашей PurchaseOrderCollection. После того, как ваши тесты подтвердят, что PurchaseOrderCollection может быть переведен в различные состояния, наиболее важными являются поведенческие тесты. Переведите вашу коллекцию заказов на покупку как в действительное, так и в недопустимое состояние любым способом, который, по вашему мнению, является подходящим (насмешка или обновление конкретных классов в желаемом состоянии), и проверьте, что логика для каждого состояния PurchaseOrderCollection выполняется правильно, а не только для PurchaseOrderCollection просто в действительном / недействительном состоянии.

PurchaseOrderCollection всегда будет зависеть от другого класса, поскольку это специализированная коллекция. Знание того, что у IPurchaseOrder есть свойство IsValid, ничем не отличается от знания о том, что конкретный объект PurchaseOrder имеет свойство IsValid. Я бы придерживался самой простой вещи, которая работает, например конкретный заказ на покупку, если у вас нет особых оснований полагать, что в вашей системе будет несколько типов заказов на покупку. На этом этапе интерфейс PurchaseOrder будет иметь больше смысла.

0 голосов
/ 26 января 2009

Являются ли обе эти действительные идеи?

Да.

Вы также можете создать Мать объекта, которая может возвращать как действительные, так и недействительные PurchaseOrders.

0 голосов
/ 26 января 2009

Я не эксперт в юнит-тестировании, но вот что я делал в прошлом. Если у вас есть класс PurchaseOder, который может быть действительным / недействительным, то я уверен, что у вас также есть модульные тесты для них, чтобы проверить, действительно ли они проверяют. Почему бы не вызвать эти методы для создания действительных и недействительных объектов PurchaseOrder, а затем добавить их в свою коллекцию?

...