Модульное тестирование без привязки к деталям реализации

16

В своем выступлении TDD, где все это пошло не так , Ян Купер выдвигает первоначальное намерение Кента Бека за модульное тестирование в TDD (для тестирования поведения, а не методов классов в частности) и высказывается за недопущение связи тестов с реализацией.

В случае поведения, подобного save X to some data sourceсистеме с типичным набором сервисов и репозиториев, как мы можем провести единичное тестирование сохранения некоторых данных на уровне сервиса через репозиторий, не привязывая тест к деталям реализации (например, вызывая определенный метод? )? Не стоит ли избегать такого рода связей на самом деле?

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

Ответы:

8

Ваш конкретный пример - это случай, который вы обычно должны проверять, проверяя, был ли вызван определенный метод, потому что saving X to data sourceозначает взаимодействие с внешней зависимостью , поэтому вы должны проверить, что общение происходит так, как ожидалось .

Тем не менее, это не плохо. Граничные интерфейсы между вашим приложением и его внешними зависимостями не являются деталями реализации , на самом деле они определены в архитектуре вашей системы; Это означает, что такая граница вряд ли изменится (или, если это необходимо, это будет наименее частый вид изменений). Таким образом, связывание ваших тестов с repositoryинтерфейсом не должно доставлять вам слишком много хлопот (если это так, подумайте, не нарушает ли интерфейс ответственность из приложения).

Теперь рассмотрим только бизнес-правила приложения, отделенного от пользовательского интерфейса, баз данных и других внешних служб. Это где вы должны быть свободны, чтобы изменить как структуру, так и поведение кода. Именно здесь сочетание тестов и деталей реализации заставит вас изменить больше тестового кода, чем производственного кода, даже если нет общего изменения в поведении приложения. Это где тестирование, Stateа не Interactionпомогает нам идти быстрее.

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

MichelHenrich
источник
Когда вы упоминаете «связь с внешней зависимостью», вы имеете в виду внешние зависимости как внешние, которые испытывают модуль, или внешние по отношению к системе в целом?
Энди Хант
Под «внешней зависимостью» я подразумеваю все, что вы можете рассматривать как плагин для вашего приложения. Под приложением я подразумеваю бизнес-правила, не зависящие от каких-либо подробностей, например, какую инфраструктуру использовать для сохранения или пользовательского интерфейса. Я думаю, что дядя Боб может объяснить это лучше, как в этом выступлении: youtube.com/watch?v=WpkDN78P884
MichelHenrich
Я думаю, что это идеальный подход, как говорится в докладе, для тестирования на основе «функции» или «поведения» и одного теста на функцию или поведение (или перестановку одного, то есть изменяющихся параметров). Однако, если у меня есть 1 «счастливый» тест для функции, для выполнения TDD это означает, что у меня будет один гигантский коммит (и проверка кода) для этой функции, что является плохой идеей. Как этого избежать? Написать часть этой функции в качестве теста и весь код, связанный с ней, а затем постепенно добавить оставшуюся часть функции в последующих коммитах?
Иордания
Мне бы очень хотелось увидеть реальный пример тестов, которые связаны с реализацией.
PositiveGuy
7

Моя интерпретация этого разговора такова:

  • тестовые компоненты, а не классы.
  • тестировать компоненты через их интерфейсные порты.

Об этом не говорится в разговоре, но я думаю, что предполагаемый контекст для совета выглядит примерно так:

  • вы разрабатываете систему для пользователей, а не, скажем, служебную библиотеку или инфраструктуру.
  • Цель тестирования состоит в том, чтобы успешно доставить как можно больше в рамках конкурентного бюджета.
  • Компоненты написаны на одном зрелом, возможно, статически типизированном языке, таком как C # / Java.
  • компонент порядка 10000-50000 строк; проект Maven или VS, плагин OSGI и т. д.
  • Компоненты написаны одним разработчиком или тесно интегрированной командой.
  • вы следуете терминологии и подходу чего-то вроде гексагональной архитектуры
  • порт компонента - это место, где вы оставляете локальный язык, а его система типов - переключение на http / SQL / XML / bytes / ...
  • Обертывание каждого порта - это типизированные интерфейсы в смысле Java / C #, в которых реализации могут быть переключены на технологии коммутации.

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

Экстраполируя это и добавляя некоторые свои мысли,

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

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

База данных, как правило, является соавтором, поэтому она притворяется, а не издевается. Это было бы больно осуществлять вручную; К счастью, такие вещи уже существуют .

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

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

Обратите внимание, что даже здесь, при условии, что вы используете безопасный для типов инструмент для пересмешивания, такой как mockito , переименование метода интерфейса не может вызвать сбой теста. Если вы используете IDE с загруженными тестами, она будет реорганизована вместе с методом переименования. Если вы этого не сделаете, тест не будет компилироваться.

Сору
источник
Можете ли вы описать / дать мне конкретный пример интерфейсного порта?
PositiveGuy
что пример выходного интерфейса. Можете ли вы быть конкретным в коде? То же самое с интерфейсом ввода.
PositiveGuy
Интерфейс (в смысле Java / C #) оборачивает порт, который может быть любым, что говорит с внешним миром (d / b, socket, http, ....). Выходной интерфейс - это тот, который не имеет методов с возвращаемыми значениями, которые приходят из внешнего мира через порт, только исключения или эквивалентные.
Сору
Интерфейс ввода противоположен, соавтор - и вход, и выход.
Сору
1
Я думаю, что вы говорите о совершенно другом подходе к дизайну и наборе терминологии, чем описано в видео. Но в 90% случаев хранилище (то есть база данных) является коллаборатором, а не входом или выходом. И поэтому интерфейс к нему является интерфейсом сотрудничества.
сору
0

Мое предложение состоит в том, чтобы использовать основанный на состоянии подход тестирования:

ДАЛИ У нас есть тестовая БД в известном состоянии

КОГДА служба вызывается с аргументами X

ПОТОМ утверждаем, что БД перешла из исходного состояния в ожидаемое состояние, вызвав методы репозитория только для чтения и проверив их возвращаемые значения

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

Единственное соединение здесь - это вызов метода сервиса и вызовы репозитория, необходимые для чтения данных из БД, что нормально.

Elifarley
источник