TDD: макетирование тесно связанных объектов

10

Иногда объекты просто должны быть тесно связаны. Например, CsvFileкласс, вероятно, должен будет тесно работать с CsvRecordклассом (или ICsvRecordинтерфейсом).

Однако из того, что я узнал в прошлом, одним из основных принципов разработки, основанной на тестировании, является «Никогда не тестируйте более одного класса за раз». Это означает, что вы должны использовать ICsvRecordмакеты или заглушки, а не фактические экземпляры CsvRecord.

Однако, попробовав такой подход, я заметил, что издевательство над CsvRecordклассом может стать немного волосатым. Что приводит меня к одному из двух выводов:

  1. Трудно писать модульные тесты! Это кодовый запах! Refactor!
  2. Копировать каждую зависимость просто неразумно.

Когда я заменил свои издевательства на настоящие CsvRecordэкземпляры, все пошло гораздо более гладко. Когда я искал мысли других людей, я наткнулся на этот пост в блоге , который, кажется, поддерживает № 2 выше. Для объектов, которые естественно тесно связаны, мы не должны сильно беспокоиться о насмешках.

Я не в курсе? Есть ли недостатки в предположении № 2 выше? Должен ли я на самом деле думать о рефакторинге моего дизайна?

Фил
источник
1
Я думаю, что это распространенное заблуждение, что «юнит» в «модульных тестах» обязательно должен быть одним классом. Я думаю, что ваш пример показывает случай, когда может быть лучше, чтобы эти два класса образовали одну единицу. Но не поймите меня неправильно, я полностью согласен с ответом Роберта Харви.
Док Браун

Ответы:

11

Если вам действительно нужна координация между этими двумя классами, напишите CsvCoordinatorкласс, который инкапсулирует ваши два класса, и проверьте это.

Тем не менее, я оспариваю понятие, CsvRecordкоторое не может быть проверено независимо. CsvRecordэто в основном класс DTO , не так ли? Это просто набор полей, возможно с парой вспомогательных методов. И CsvRecordможет использоваться в других контекстах, кроме того CsvFile; Вы можете иметь коллекцию или массив CsvRecords, например.

Проверьте CsvRecordсначала. Убедитесь, что он проходит все свои тесты. Затем продолжайте и используйте CsvRecordс вашим CsvFileклассом во время теста. Используйте его в качестве предварительно протестированной заглушки / макета; заполните его соответствующими тестовыми данными, передайте его CsvFileи напишите свои тестовые примеры на этот счет .

Роберт Харви
источник
1
Да, CsvRecord, безусловно, может тестироваться независимо. Проблема в том, что если что-то сломается в CsvRecord, это приведет к сбою тестов CsvData. Но я не думаю, что это серьезная проблема.
Фил
1
Я думаю, вы хотите, чтобы это произошло. :)
Роберт Харви
1
@RobertHarvey: теоретически, это может стать проблемой, если CsvRecord и CsvFile становятся довольно сложными классами, и если тест прервется для CsvFile, теперь вы сразу не знаете, если это проблема в CsvFile или CsvRecord. Но я думаю, что это скорее гипотетический случай - если бы у меня была задача программирования таких классов для реальной программы, я бы сделал это именно так, как вы это описываете.
Док Браун
2
@Phil: если CsvRecordперерывы, то, очевидно, CsvDataне получается; но это нормально, потому что вы CsvRecordсначала тестируете , а если это не удается, ваши CsvFileтесты не имеют смысла. Вы все еще можете различать ошибки в CsvRecordи в CsvFile.
tdammers
5

Причина тестирования по одному классу за раз заключается в том, что вы не хотите, чтобы тесты для одного класса имели зависимости от поведения второго класса. Это означает, что если ваш тест для класса A выполняет какие-либо функции класса B, то вам следует смоделировать класс B, чтобы удалить зависимость от конкретной функции в классе B.

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

Но если у CsvRecordнего нет собственной логики, то нечего его высмеивать. Это на самом деле просто старая изречение - «не издевайтесь над объектами значения» .

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

Дауд ибн Карим
источник
+1. Любой тест, результат которого зависит от правильности поведения нескольких объектов, является интеграционным, а не модульным тестом. Вы должны смоделировать один из этих объектов, чтобы получить настоящий юнит-тест. Это не относится к объектам, в которых нет реального поведения, - например, только с геттерами и сеттерами.
guillaume31
1

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

Telastyn
источник
0

«Связанные» классы взаимозависимы друг от друга. Это не должно относиться к тому, что вы описываете - CsvRecord на самом деле не должен заботиться о CsvFile, содержащем его, поэтому зависимость идет только в одну сторону. Это нормально, и не жесткая связь.

В конце концов, если класс содержит переменную String name, вы бы не утверждали, что она тесно связана со String, не так ли?

Итак, юнит-тест CsvRecord для его желаемого поведения.

Затем используйте макет (Mockito отлично), чтобы проверить, взаимодействует ли ваше подразделение с объектами, от которых оно зависит. Поведение, которое вы хотите протестировать, действительно - это CsvFile обрабатывает CsvRcords ожидаемым образом. Внутренняя работа CvsRecord не должна иметь значения - это то, как CvsFile взаимодействует с ним.

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

Мэтью Флинн
источник
1
-1, тесная связь не обязательно означает циклические зависимости, это заблуждение. В этом примере CsvFile он тесно связан CsvRecord(но не наоборот). OP спрашивает , если это хорошая идея , чтобы проверить CsvFileпутем отделения его от CsvRecordПОСРЕДСТВОМ ICsvRecord, а не наоборот.
Док Браун
2
@DocBrown: является ли связь жесткой или нет, зависит от того, насколько многое CsvFileзависит от внутренней работы CsvRecord, то есть от количества предположений, которые файл имеет относительно записи. Интерфейсы помогают документировать и применять такие допущения (или, скорее, отсутствие других допущений), но степень связывания остается неизменной, за исключением того, что с интерфейсом вы можете подключить другой класс записей CsvFile. Представлять интерфейс просто так, чтобы вы могли сказать, что у вас снижена связь, глупо.
tdammers
0

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

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

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

Большинство людей, кажется, предполагают, что вы относитесь к CsvRecordклассу ценности. Попробуйте сделать это один. Сделайте его неизменным, если можете. Если у вас есть два объекта с указателями друг на друга, удалите один из них и выясните, как заставить его работать. Ищите места для разделения классов и функций. Лучшее место для разделения класса не всегда совпадает с физическим расположением файла. Попробуйте поменять отношения родитель / потомок классов. Может быть, вам нужен отдельный класс для чтения и записи CSV-файлов. Возможно, вам нужны отдельные классы для обработки файлового ввода-вывода и интерфейса с верхними уровнями. Есть много вещей, чтобы попробовать, прежде чем объявить это нерефрактивным.

Карл Билефельдт
источник