Pull to refresh

Comments 99

Давайте вспомним рабочий процесс в рамках TDD: «красные» тесты, сигнализирующие об ошибке -> создание\изменение функционала -> «зелёные» тесты. Соответственно, программист сначала изменяет тесты так, чтобы они тестировали новый функционал.

Воу воу. Где это написано что програмист меняет тест чтобы написать новый функционал?? Программист пишет новый тест, который будет тестировать новый функционал. Я бы не стал ломать работающие тесты если бы захотел реализовать новый функционал. Потом может быть я удалю старые, если посчитаю их избыточными. А может актуализирую под новый функционал. Но менять…
На стадии сопровождения часто требуется изменить действующий функционал. Например, новую колонку добавить в уже существующий отчёт, новый перк добавить уже существующему персонажу, внешнему сервису, с которым уже налажено взаимодействие, какие-то другие передать и так далее. В таких случаях зачастую проще изменить существующий тест и существующий класс, чем стереть тест и класс и написать всё заново. Это не ломание рабочего теста и рабочего класса, а просто приведение их в соответствие с новой бизнес-логикой и новыми требованиями заказчика.
Зачем стирать тесты? Зачем их ломать? Зачем изменять? Новому функционалу — новые тесты, не?
Представьте такую ситуацию. Гейм-дизайнеры решили, что какой-то перк (или способность) слишком сильная, и для соблюдения баланса решили уменьшить её эффективность в 2 раза. Или путём уменьшения в 2 раза какой-то характеристики перка, или путём добавления каких-то новых условий, которые необходимы, чтобы перк сработал. При этом этот перк должен остаться у тех же персонажей, которые им владели изначально. В этой ситуации проще в коде класса существующего перка константу изменить или условие добавить, чем удалять отовсюду старый перк и добавлять новый. А раз меняется бизнес-логика работы класса, то и тест нужно изменить. При этом в рамках TDD тест нужно изменить в первую очередь.
Ситуации разные бывают. Иногда при изменении функционала нужно существующие тесты изменить, иногда их нужно удалить, иногда нужно новые написать, не трогая существующих, — всё от ситуации зависит. Но если вы меняете логику работы класса, то нужно в любом случае взглянуть и на его тесты, и что-то с этими тестами сделать (добавить\удалить\изменить)
Понятно, просто выше вы писали о новом функционале:

> новую колонку добавить в уже существующий отчёт
> новый перк добавить уже существующему персонажу

Да, вполне вероятно, что где-то надо будет тесты менять, просто у вас это (и в посте) вроде как по-умолчанию.

UPD: А, вижу вы в посте указали, что при изменении, но получилось совсем странно:

> Соответственно, при изменении функционала, программист сначала изменяет тесты так, чтобы они тестировали новый функционал.

Очевидно, не новый функционал, а изменённый.
Это понятно, но это наверное не совсем про TDD.
Почему? Грубо говоря, был тест: «в возвращаемом отчете есть колонки а, б, ц». Изменились требования, теперь в отчете должна еще появиться колонка «д». Меняем тест (он стал «в возвращаемом отчете есть колонки а, б, ц, д»), он становится красным, меняем реализацию, тест зеленый. Все. И тест продолжает соответствовать требованию.
> Во-первых, при изменении функционала сложно вообще вспомнить, что помимо класса и тестов на него нужно изменить ещё и моки этого класса

Во-первых, надо разделять классы так, что бы это не было проблемой. В случае, если вы тут не будете использовать моки — у вас при изменении поведения упадёт 1000 тестов. Да, заметить это легче, но чинить сложнее.

> Как видите, в этом определении нет ни слова о том, что интеграционные тесты можно писать только на главный класс
> Согласно TDD, тесты предназначены для проверки функционала (feature)

Во-вторых, вы зачем-то смешиваете «classical TDD» и интеграционные тесты. Это совершенно разные вещи. Поэтому там написано именно то, что написано. Интергационный тест — это более чем про одну feature, и к TDD имеет очень отдалённое отношение.
Во-первых, надо разделять классы так, что бы это не было проблемой.
А можете пояснить, как именно нужно разделить классы? Не представляю, как перераспределение кода по классам способно решить эту идеологическую проблему, если изменение моков не вписывается ни в один из этапов TDD-шного цикла.

В случае, если вы тут не будете использовать моки — у вас при изменении поведения упадёт 1000 тестов. Да, заметить это легче, но чинить сложнее.
Этот вопрос подробно рассматривался в статье.

Во-вторых, вы зачем-то смешиваете «classical TDD» и интеграционные тесты. Это совершенно разные вещи.
Тест — это фрагмент кода, TDD — это философия, рабочий процесс. Интеграционные тесты используются в классическом TDD. Я не говорил, что тест = TDD.

Интергационный тест — это более чем про одну feature, и к TDD имеет очень отдалённое отношение.
А можете пояснить, почему по-вашему интеграционные тесты не имеют отношения к TDD?
> А можете пояснить, как именно нужно разделить классы?

Например, http://martinfowler.com/bliki/TellDontAsk.html
Если у вас код не зависит от поведения класса мока, то вам не надо править мок.

> Интеграционные тесты используются в классическом TDD

И вы легко приведёте пруфы?

> А можете пояснить, почему по-вашему интеграционные тесты не имеют отношения к TDD?

А я такого не писал.
Если у вас код не зависит от поведения класса мока, то вам не надо править мок.
Речь шла не о том, в каких случаях нужно править мок, а о том, как решить, что вот прямо сейчас пришло время поправить мок. В рамках TDD все этапы чётко расписаны, что и после чего нужно делать. И правка моков не вписывается ни в один этап.

И вы легко приведёте пруфы?
Пруфы содержаться в статье. Ссылка на определение интеграционного теста и ссылка на определение классического и мокисткого TDD.
> Речь шла не о том, в каких случаях нужно править мок, а о том, как решить, что вот прямо сейчас пришло время поправить мок. В рамках TDD все этапы чётко расписаны, что и после чего нужно делать. И правка моков не вписывается ни в один этап.

Ну вы же сами себе отвечаете: не надо править мок, нет такого этапа.

> Ссылка на определение интеграционного теста

И как это обосновывает применимость его в философии TDD? Вы или выражайтесь конкретнее, или одно из двух.

> ссылка на определение классического и мокисткого TDD

Вы опять всё смешали. TDD, классический TDD и мокисткий TDD — 3 разные вещи. Да и определения как такового там нет.
Ну вы же сами себе отвечаете: не надо править мок, нет такого этапа.
Так в том то и дело, что этапа нету, а править надо, раз поведение мокированного класса изменилось.

И как это обосновывает применимость его в философии TDD?
По Фаулеру, The classical TDD style is to use real objects if possible and a double if it's awkward to use the real thing. Поскольку real objects, то тест интеграционный.
> а править надо, раз поведение мокированного класса изменилось.

Это не причинно-следственная связь. Это косяк дизайна. TDD эту проблему решать не задуман.

> Поскольку real objects, то тест интеграционный.

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

Если все женщины люди
Не если, а безусловно люди. Раз вы о прекрасном поле заговорили, то, я так понимаю, тема применения интеграционных и модульных тестов в классическом TDD исчерпана?
> Я об этом в статье и пишу, и предлагаю или TDD-шный цикл разработки изменить, или не мочить всё подряд без проверки задекларированного в моках поведения.

Вы в TDD можете хоть кодревью добавить, хоть необходимость завтракать. Только это будет уже не TDD.

> Поскольку real objects, то тест интеграционный.

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

Вывод неправильный.

Ладно, побуду занудой. Докажем, что интеграционные тесты используются в классическом TDD. Раз 1)классическое TDD, согласно Фаулеру, призывает использовать в тестах реальные объекты там, где это возможно и 2)в программировании возможна ситуация, что в методе одного класса используется метод другого класса, и нет веских обстоятельств, мешающих использовать реальный объект (пункт 2 доказывается примером, который я не считаю нужным приводить), то из этих двух пунктов следует, что в классическом TDD возможна ситуация тестирования одного модуля, обращающегося к другому. А по определению интеграционного теста, это интеграционный тест. Следовательно, интеграционные тесты используются в классическом TDD. Теперь согласны? Или снова будете говорит что всё неправильно, не поясняя где вы заподозрили противоречие?
из Ваших определений следует, что поделив тестируемый метод пополам, без изменения функционала, мы получим интеграционный тест. Если вспомнить, что определение Фаулера не содекржит упоминаний, что использование «стандартных» классов нещитово — вы своим доказательством фактически отрицаете существование на практике модульных тестов.

Да и не стоит приравнивать «класс» и «модуль»
из Ваших определений следует, что поделив тестируемый метод пополам, без изменения функционала, мы получим интеграционный тест
Если переместить часть кода тестируемого метода в другие классы, в тестируемом методе вызвать методы этих других классов, и не навесить моки на эти классы, то модульный тест станет интеграционным. Это следует из определений модульного и интеграционного теста на википедии.
Если вспомнить, что определение Фаулера не содекржит упоминаний, что использование «стандартных» классов нещитово
Зато это следует из определений модульного теста и исходного кода программы на википедии. (исходный код != (не равно) язык программирования. исходный код — то, что написано на языке программирования)

Извините, мне не интересно с вами общаться. До свидания.
То есть использование БД не противоречит модульному тесту, а использование приватного класса внутри класса — противоречит. Замечательно.
Многие юнит-тесты с моками действительно становятся сложными в поддержке.
Но, по-моему, при полноценном TDD сначала пишется интеграционный тест, а затем, при необходимости, юнит-тесты на уровень ниже.
Подходы только юнит или только интеграционные — это две крайности.
при полноценном TDD сначала пишется интеграционный тест, а затем, при необходимости, юнит-тесты на уровень ниже.
А почему не «а затем, при необходимости, интеграционные тесты на уровень ниже»?

Подходы только юнит или только интеграционные — это две крайности.
Довольно спорное философское утверждение. А быть негром или европеоидом — это тоже две крайности? Есть две школы (классическая и мокисты), которые по-разному подходят к TDD. В какой-то фиксированный момент времени вы либо ведёте себя как последователь одной школы, либо как последователь другой. В противном случае вы ведёте себя как человек, не следующий учению ни одной из школ, и вы можете основать свою школу, если сумеете систематизировать свои знания и формально описать новый подход (а не просто сказав, что выбираем тип теста в зависимости от дня недели, не объяснив, почему модульные тесты лучше писать по вторникам, а интеграционные — по пятницам). Возможно эта новая школа будет сочетать лучшие традиции предыдущих двух школ, и они исчезнут, слившись в одну. Но пока что этого не произошло. При всём уважении, желание быть всегда «где-то посередине» — тоже крайность, имхо.
Мокисткое правило — «Всегда мокируйте любое поведение объектов». Правило классического TDD — «Всегда используйте настоящие объекты, если это возможно». Если вы изобретёте новое правило, то это будет новым подходом. Не представляю, как можно быть где-то посередине: использовать немного настоящий объект, который является немного мокой. Может, можно как-то извратиться и сделать, но не представляю зачем и как.
Хотя, в чём-то вы правы :) В проекте в любом случае будет содержаться некоторая доля модульных тестов: для тех классов, внутри которых нет обращений к методам других классов. Формально и фактически такие тесты не будут интеграционными. Они будут потенциально интеграционными. Если в них добавится обращение к методам других классов, то по классическому TDD эти тесты будут преобразованы и станут интеграционными фактически. В статье шло скорее не сравнение интеграционных тестов и модульных, а сравнение мокисткого подхода и классического. Раз это не так очевидно, то добавлю в статью, что даже при классическом подходе какая-то часть тестов будет модульными. Хотя, имхо, это и так очевидно, вроде бы.
> Если в них добавится обращение к методам других классов, то по классическому TDD эти тесты будут преобразованы и станут интеграционными фактически

Если вы тестируете string ReverseString(string) — вы считаете это будет интеграционный или модульный?
Если тестировать методом черного ящика и писать тест до реализации, то непонятно, получится он интеграционным или модульным — зависит от того, обращается ли к методам других классов ReverseString. Тип теста можно будет узнать только после реализации ReverseString. Кот Шрёдингера какой-то получается.
ReverseString — метод. Для простоты, статический и один в классе.
Если взять за определения, что интеграционный тест работает без искусственных заглушек, а модульный не требует присутствия зависимостей тестируемого класса, то такой тест удовлетворяет обоим определениям.
А если взять определения из википедии?
При TDD разработка мне видится так:
1. при необходимости внесения изменений человек описывает парочку интеграционных тестов, если это возможно, которые показывают как всё должно работать в целом.
2. начинает вносить изменения, если в итоге изменения небольшие и покрываются тем что он написал, то всё хорошо.
3. если человек начинает терять контроль над кодом в плане тестов (например, внутри объекта идёт сложная агрегация из ещё чего-то), то человек тестирует и эти модули.

Конечно же, это «идеальный вариант» на практике часто пишутся сначала «кусочки» все или не все, которые потом объединяются во что-то большее. Но сама идеология TDD, по-моему, подразумевает разработку от общего к частному, а не наоборот. Но, возможно, это уже BDD.

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

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

В статье приведена ссылка на статью из русскоязычной википедии, в которой дано следующее определение: модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы. (Соответственно в статье я подразумеваю под модульным тестированием именно это.) Также приведена ссылка на определение интеграционного теста: одна из фаз тестирования программного обеспечения, при которой отдельные программные модули объединяются и тестируются в группе.

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

При TDD разработка мне видится так:
1. при необходимости внесения изменений человек описывает парочку интеграционных тестов, если это возможно, которые показывают как всё должно работать в целом.
Нет, в рамках TDD тесты пишутся не на «всё в целом», а на конкретный функционал. При этом в классическом TDD игнорируется тот факт, что этот функционал может использовать другой функционал, а в мокистком TDD эта связь с другим функционалом разрывается. Это важное различие между классическим и мокистким TDD. Классическое TDD проверяет корректность функционала, а мокисткое — корректность поведения класса.
А если я вместо стандартной библиотеки времени использую нестандартную, то уже не модульное?

Зачем вы цепляетесь к словам «всё в целом»? Конечно же имелось ввиду как должен «весь в целом» работать функционал.
А если я вместо стандартной библиотеки времени использую нестандартную, то уже не модульное?
Совершенно верно.
Допустим, а как быть, если язык позволяет патчить стандартные библиотеки напрямую и мы используем данную стандартную, но пропатченную библиотеку?
Тесты тестируют модули исходного кода. Когда вы патчите стандартную библиотеку, появляется исходный код. Раз несколько модулей исходного кода — значит тест интеграционный.
Хотя, если есть возможность пропатчить стандартную библиотеку применительно к только какому-то одному модулю (классу), и есть возможность сделать это внутри самого модуля, то тестируется содержимое одного модуля, и тест модульный.
Вообще, не вижу смысла париться над разделением интеграционные тесты — модульные. Актуальнее проблема классическое TDD — мокисткое.
> Хотя, если есть возможность пропатчить стандартную библиотеку применительно к только какому-то одному модулю (классу), и есть возможность сделать это внутри самого модуля, то тестируется содержимое одного модуля, и тест модульный.

Спасибо, вот теперь всё встало на места. Если весь код написать в одном классе — будут идеальные модульные тесты.
Вот ситуация.

Я тестирую какой-то объект. Внутри него используется ещё 1.
Он прямо там и создаётся, больше нигде не используется. Я его решил не тестировать, потому что он, к примеру, слишком прост, считает мне 2 + 2. Я не хочу его передавать через IoC или как-то ещё. Я даже имею возможность его замочить его без IoC, но не хочу этого делать. Судя по-Вашему, это интеграционный тест.

Но стоит мне только перенести весь функционал из этого вспомогательного объекта в основной, так тест становится модульным?

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

[Ирония on]Недавно прочитал на микроволновке «не помещать внутрь домашних животных», а у меня на балконе карликовый жираф живёт. Балкон — хоть и часть дома, но такая незначительная, прям вообще не часть дома. Я там кроме рассады и жирафа ничего не держу. Какая же это часть дома?! Это часть природы! Да и жираф — милый такой, совсем не животное. Помыл я жирафа, и решил посушить его в микроволновке, а она возьми и сломайся! Но в гарантийном ремонте мне отказали. Не хотят соглашаться, что жираф с балкона — не домашнее животное. Ну просто глупцы какие-то! [Ирония off]

Надеюсь вас история повеселила.
P.S. История придуманная. Зоозащитники, узбагойтесь.
А вам самому не кажется странным, что тест то модульный, то сразу интеграционный и это постоянно меняется, причём при изменении не теста, а реализации?

И я уже задавал вопрос — почему при использовании стандартных классов он остаётся модульным, а при нестандартных перестаёт таковым быть? Разве модульность/интеграционность определяется набором библиотек?
И какое определение модульного теста говорит, что в внутри него не должно быть зависимостей? Или получается, что могут быть, но только некоторые?
Определение модульного теста определяет тестирование изолированного модуля, юнита, некой абстракции с которой мы работаем как с чёрным ящиком — не более.

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

P.S. сомневаюсь что существуют миллиарды людей, которые используют определения модульного/интеграционного тестирования.
А вам самому не кажется странным, что тест то модульный, то сразу интеграционный и это постоянно меняется, причём при изменении не теста, а реализации?
Нет, не кажется. Если тип теста (интеграционный \ модульный) зависит от реализации того, что он тестирует (по определению теста) и от реализации самого теста (ухода от реализации «лишних» классов в тесте за счёт мокирования), то вполне логично, что при изменении одной из реализаций или сразу двух тип теста может измениться.

И я уже задавал вопрос — почему при использовании стандартных классов он остаётся модульным, а при нестандартных перестаёт таковым быть?
На этот вопрос я уже отвечал. Если результат теста зависит от реализации нескольких модулей исходного кода программы, то тест интеграционный (по определению). Исходный код — текст программы на языке программирования. Язык программирования и исходный код программы — это не одно и тоже. Стандартные классы (int,bool,string,DateTime и пр.) — это часть языка, а не модули исходного кода программы.

Вообще, по поводу некоторых терминов можно долго и безрезультатно спорить. Особенно в молодых или неточных науках, где терминология не до конца устоялась и со временем претерпевает изменения, и один и тот же термин может означать немного разные явления в разных кругах. Не исключено, что до появления ООП под интеграционным тестированием понималось нечто другое. Также не исключено, что некоторые люди до сих пор понимают под интеграционным тестированием тоже самое, что понимали до появления ООП. Что под интеграционным тестированием понимаю лично я и те программисты, с которыми я общался, я вам рассказал. И, судя по этой статье, некоторые заокеанские коллеги понимают под интеграционным тестированием тоже самое. Можно конечно в начало статьи поместить длиннющий список определений, что подразумевается под мокой, мокированием, тестом, функционалом, рабочим процессом и прочим. Но что под этим подразумевается и так ясно из контекста. Статьи по программированию — это, как правило, не строгое научное доказательство программистких теорем в виде кванторов, а передача опыта и идей. Я бы не хотел читать статьи, в которых половина текста посвящена определениям исходного кода, рабочего процесса и прочего.

В статье было сказано, что понимается под интеграционным тестом (I use the term integrated test to mean any test whose result (pass or fail) depends on the correctness of the implementation of more than one piece of non-trivial behavior.) и шли рассуждения, почему интеграционных тестов не надо бояться. По сути статьи вопросы есть?
> Первый написанный тест может быть красным, пока другие тесты на классы, которые находятся слоем ниже, пишутся и зеленеют.

Это вы тоже в википедии нашли?
Всё, что мы знаем при тестировании с использованием моков — это то, что у нас есть хорошо работающий мок. О том, что произойдёт с этим всем кодом на боевой системе мы не знаем ничего. Это как протестировать 10 000 кирпичей по отдельности и на этом основании дать гарантию, что построенный из них дом будет абсолютно надёжным. В общем, я за интеграционные тесты + модульные тесты в тех случаях, когда мы тестируем реальный код, а не моки.
Спасибо за статью.
А вот и картинку в тему
image
А если серьезно по теме. Я не против интеграционного тестирования, хотя на мой взгляд интеграционное тестирование имеет больше минусов чем плюсов по сравнению с модульным. Недавно обсуждались проблемы black-box testing и преимущества white-box testing.

Высказывание 1. Интеграционные тесты в меньшей степени помогают в поиске ошибок, нежели юнит-тесты

Тут довольно спорный вопрос больше или меньше.
Например, если меняется внутренняя реализация метода isEqual возвращающая в некоторых случаях false там где возвращала true, то у нас поломается только 1 юнит-тест и нужно будет дополнить его в связи с изменениями. В случае же интеграционного тестирования у нас может половина тестов покраснеть, хотя реально сделанные изменения ничего не ломают и ни как не влияют на корректность результата.

Из ваших же примеров. Добавление колонки в результат означает изменение теста измененного метода, изменение методов которые используют измененный метод, изменение тестов которые используют измененный метод т.д. В не зависимости от метода тестирования придется менять все. В случае с интеграционными тестами тесты которые нужно менять сразу покраснеют. В случае с юнит-тестами эти тесты придется искать через поиск, но это не проблема (Ctrl+F и название метода/класса), а поскольку тестов в случае юнит-тестирования в разы меньше то и результатов в поиске будет совсем чуть-чуть и тесты будут исправлены быстрее чем в случае интеграционного тестирования.

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

Высказывание 2. Интеграционные тесты в меньшей степени помогают в проектировании, нежели модульные

Это вообще не относится к проектированию и тестированию. Просто везде используется DI и все. А как вы будите тестировать класс с зависимостями это уже другая история.

Высказывание 3. Чтобы покрыть тестами один и тот же функционал, интеграционных тестов потребуется гораздо больше, чем модульных

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

Честно говоря я не понял как при интеграционном тестировании мы не столкнемся с экспоненциальным увеличением количества тестов. Ведь мы же используем реальные зависимости, а не моки, соответственно мы должны протестировать не только функциональность тестируемого метода, но и функциональность используемых зависимостей (в этом же весь смысл), а наши зависимости могут иметь свои зависимости, которые тоже могут иметь зависимости и т.д. Вот уже и получаем экспоненциальное увеличение. Это известная проблема black-box testing.

Простая математика:
  • для тестирования метода нужно написать 5 тестов
  • метод имеет 2 зависимости и для тестирования каждой из них нужно еще по 5 тестов
  • первая из зависимостей имеет тоже 2 зависимости и для тестирования каждой из них нужно еще 5

Итог: 5 * 5 * 5 * 5 * 5 = 3125 тестов для того что бы покрыть 1 единственный метод против 5 тестов в случае юнит-тестирования. 3125 тестов Карл.
И это еще простой пример с малым количеством зависимостей. Подсчитал тут интереса ради для одного своего реального метода и получил примерно 15504 теста против 57 юнит-теста.

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

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

Есть у вас в программе Петя и John, и функция isEqual, которая их различает. И вот в один прекрасный день вы изменяете isEqual и тест на неё, и теперь она их не различает. А ещё есть тест на то, что жена Пети не пускает в три часа ночи незнакомых пьяных мужчин. И вот после изменения isEqual этот тест краснеет. Вы ругаете интеграционное тестирование, и удаляете этот тест, ведь «сделанные изменения ничего не ломают и ни как не влияют на корректность результата». Примерно через год после этого приходит Петя и слёзно спрашивает вас, почему его жена пускает по ночам пьяных незнакомых мужчин, и почему в его семье родился негр, хотя ни у него в роду, ни у его жены негров отродясь не было.

Это вообще не относится к проектированию и тестированию. Просто везде используется DI и все.
Если DI и IOC не имеют никакого отношения к проектированию, то я — испанский лётчик.

Ведь мы же используем реальные зависимости, а не моки, соответственно мы должны протестировать не только функциональность тестируемого метода, но и функциональность используемых зависимостей (в этом же весь смысл).
Прочитайте определение интеграционного теста… Там не сказано, что целью интеграционного теста является тестирование нескольких модулей. Там сказано, что результат прохождения (зелёный или красный) зависит от корректности реализации нескольких модулей (или что модули тестируются в группе). Это большая разница. Определение диктует то, как интеграционный тест должен быть реализован, но не то, с какой целью вы будете его реализовывать, и как вы будете его использовать. Цель определяет TDD. Интеграционные и модульные тесты в плане цели в TDD не различаются — это тестирование одной функциональности. Допустим есть функционал А, Б, В. Каждый функционал реализован в отдельном классе. Код, реализующий функционал А, использует функционал Б. Код, реализующий функционал Б, использует функционал В. Соответственно вы можете написать 6 модульных тестов на А и Б, по 3 на штуку, и точно также можете вместо них написать 6 интеграционных тестов на А и Б. Если код, реализующий функционал В, не использует никакой другой функционал и никакие другие классы, то на функционал В можно написать только модульные тесты. И никто вас не поколотит, если вы после замены моков на реальные классы не напишите ещё 100-500 тестов. Если Б сломается, то интеграционный тест на А упадёт, а модульный — нет (вспоминаем историю с Петей и негром).
Есть у вас в программе Петя и John, и функция isEqual, которая их различает. И вот в один прекрасный день вы изменяете isEqual и тест на неё, и теперь она их не различает. А ещё есть тест на то, что жена Пети не пускает в три часа ночи незнакомых пьяных мужчин. И вот после изменения isEqual этот тест краснеет. Вы ругаете интеграционное тестирование, и удаляете этот тест, ведь «сделанные изменения ничего не ломают и ни как не влияют на корректность результата». Примерно через год после этого приходит Петя и слёзно спрашивает вас, почему его жена пускает по ночам пьяных незнакомых мужчин, и почему в его семье родился негр, хотя ни у него в роду, ни у его жены негров отродясь не было.

Об этом я говорил ниже. Интеграционные тесты позволяют тестировать контекст в котором вызывается тестируемая функция, но это не всегда нужно. Например у нас есть функция А которая проверяет какое-то условие и есть функция Б которая использует ее и с ее помощью определяет нужно ли запустить подпрограмму С. И вот мы решили изменить условия в функции А. Мы все также знаем чту функция А может возвращать true|false в разных условиях, это мы проверили через тесты. Это значит что функция Б все также будет запускать подпрограмму С, но уже в других ситуациях и это нормально. Так и должно быть.

Можно написать интеграционный тест который будет гарантировать мне что в определенных условиях функция А вернет true и функция Б соответственно запустит подпрограмму С. То есть через интеграционные тесты можно задать жесткое поведение программы. Описать все варианты развития сценария. Шаг в право, шаг в лево — расстрел. Любое изменение в коде означает изменение десятка, а то и сотни тестов.
С модульными тестами у нас больше свободы, писать их быстрее и проще, тестов меньше и выполняются они быстрее, поддерживать их в зеленом состоянии проще и меньше затрат ресурсов.

Я не спорю, интеграционные тесты это хорошо и они нужны, но они требуют столько ресурсов что выгода может не окупится. Я не говорю что их не надо писать, я говорю что нужно начать с малого, с модульных тестов. И нужно оценивать свои ресурсы. Многие компании не готовы выделить время и деньги на написание тестов вместо того что бы писать новый функционал и приносить в компании больше денег. Есть такие которые готовы потерять N $ например на уязвимости в проекте и заработать на новом функционале N^3 $.

Если DI и IOC не имеют никакого отношения к проектированию, то я — испанский лётчик.

к тестированию не имеют отношения, к тестированию

Код, реализующий функционал А, использует функционал Б. Код, реализующий функционал Б, использует функционал В. Соответственно вы можете написать 6 модульных тестов на А и Б, по 3 на штуку, и точно также можете вместо них написать 6 интеграционных тестов на А и Б.

да, только здесь умножение, а не сложение:

  • Функция А имеет 3 ветки развития алгоритма.
  • Функция А использует функцию Б.
  • Функция Б имеет 3 ветки развития алгоритма.
  • Соответственно для каждой ветки развития в функции А есть по 3 ветки развития из функции Б.
  • 3 * 3
  • Функция Б использует функцию В.
  • Функция В имеет 3 ветки развития алгоритма.
  • Соответственно для каждой ветки развития в функции Б есть по 3 ветки развития из функции В.
  • 3 * 3
  • Соответственно для каждой ветки развития в функции А есть по 3 ветки развития из функции Б и для каждой из них есть по 3 ветки развития из функции В.
  • 3 * 3 * 3

Итог: 3 * 3 * 3 = 27 веток развития алгоритма и как результат 27 вариантов результат и как результат 27 тестов функции А.

Естественно это идеализированный пример ибо не в каждой ветке развития алгоритма будет использоваться вложенная функция, но общая мысль должна быть понятна.
Любое изменение в коде означает изменение десятка, а то и сотни тестов.
Ответ на этот вопрос есть в статье. Я приводил ссылку на test builder. Нужно будет изменить builder, а не сотни тестов.

Соответственно для каждой ветки развития в функции А есть по 3 ветки развития из функции Б.
3 * 3
Этот вопрос разбирался в статье: «Согласно TDD, тесты предназначены для проверки функционала (feature), а не путей исполнения программы.». Не считайте пути исполнения программы (или ветки развития, как вы их называете), считайте сколько тестов вам нужно для тестирования конкретного функционала.

к тестированию не имеют отношения, к тестированию
Изначально было так:
Я (в статье): Высказывание 2. Интеграционные тесты в меньшей степени помогают в проектировании, нежели модульные…
Вы: Это вообще не относится к проектированию и тестированию. Просто везде используется DI и все.
Я: Если DI и IOC не имеют никакого отношения к проектированию, то я — испанский лётчик.
Вы: к тестированию не имеют отношения, к тестированию

Что-то я ничего не понял. По-вашему, тесты к тестированию не имеют отношения? Что вы вообще хотите спросить\сказать?
Если что, речь шла про тесты в рамках TDD.
Этот вопрос разбирался в статье: «Согласно TDD, тесты предназначены для проверки функционала (feature), а не путей исполнения программы.». Не считайте пути исполнения программы (или ветки развития, как вы их называете), считайте сколько тестов вам нужно для тестирования конкретного функционала.

Хорошо. Тогда объясните что вы имеете в виду под словом функционал (feature)? Тестирование только особенностей тестируемого метода, без привязки к зависимостям? Если да, то тестирование получается не полным как и в случае с модульным. И мы опять возвращаемся к проблеме с Петей и John, ибо неполный тест может не отлавливать ситуации при которых Петя == John.

Что-то я ничего не понял. По-вашему, тесты к тестированию не имеют отношения? Что вы вообще хотите спросить\сказать?
Если что, речь шла про тесты в рамках TDD.

я говорил вот об этом
Доказательство:
Модульное тестирование, в отличие от интеграционного, вынуждает программистов инжектировать зависимости через конструктор или свойства. А если использовать интеграционное тестирование вместо модульного, то джуниор может зависимости прямо в коде класса инстанцировать.

Если повсеместно использовать DI то этой проблемы не будет и не будет разницы какие тесты вы пишете, интеграционные или модульные. Это организация процесса разработки и архитектуры приложения, ну и обучения джунов. Этот вопрос не имеет прямого отношения к тестированию и TDD.
Тестирование только особенностей тестируемого метода, без привязки к зависимостям?
Я предлагал использовать вместо зависимости моки внешних сервисов и хранилищ данных, и реальные классы во всех остальных случаях. В случае с Джоном и Петей мы имеем дело с пользовательской историей (т.е. требованием, написанным на языке пользователя) «Замужняя дама должна отклонять просьбу зайти в дом в ночное время от мужчины, не являющегося её мужем». Эта пользовательская история закрепляется тестом. В тесте или при помощи билдера создаётся замужняя дама, или просто в свойство «Муж» передаётся Петя, и потом идёт сама проверка. Допустим, логика проверки на мужа была завязана на isEqual с Петей. В какой-то момент было решено, что в программе представляет интерес только, допустим, надёжность или репутация человека, и два человека с одинаковыми уровнями надёжности можно взаимозаменить в любом месте программы, и isEqual переписали. А про логику замужних дам забыли. Если использовать в качестве isEqual реальную реализацию (или функционал сравнения), то тест может отловить ошибку. А если в тесте использовать не Петю и Джона, а Петю и Васю, то по каким-то причинам может и не отловить. Это тоже ещё одна проблема, что тесты не гарантируют 100% надёжности, поскольку тестируются какие-то отдельные случаи, но не все возможные.

Если повсеместно использовать DI то этой проблемы не будет и не будет разницы какие тесты вы пишете, интеграционные или модульные.
Я примерно о том же самом в статье писал.
Это тоже ещё одна проблема, что тесты не гарантируют 100% надёжности, поскольку тестируются какие-то отдельные случаи, но не все возможные.

В таком случае интеграционные тесты имеют чуть больший процент покрытия чем модульные, но все так же далеки от 100%.
считайте сколько тестов вам нужно для тестирования конкретного функционала.
Другими словами можно сказать — сколько тестов нужно для тестирования контракта метода (выполнения постусловий при соблюдении предусловий).
Это все будет работать только для относительно простых завимостей, инстанциирование которых достаточно легкое и не требует настройки. Например, если у вас есть свой сериализатор, наверное не имеет большого смысла заменять его моком. Из моего опыта: обычно никто для таких объектов моки и не создает.

Ситуация меняется, когда зависимость имеет свои зависимости, часть из которых кстати может быть внешними. И в таком случае вам вместо того, чтобы например «замочить» один какой-нибудь вызов Validate, придется разобраться, что там этот Validate делает, к каким сервисам обращается и все это настроить. И приведет это к тому, что Arrange фаза такого теста может разрастись до огромных размеров.

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

Добавлю пару мыслей:

1. Нет смысла разделять модульные тесты и интеграционные — это лишь частные и сильно ограниченные случаи приёмочных тестов.

2. Зависимости между модулями должны выстраиваться в дерево. Сборщик сериализует это дерево и инициализирует модули в порядке от менее зависимых, до более зависимых. В том же порядке должны запускаться и приёмочные тесты. Таким образом первый упавший тест укажет на проблемное место. По мере прохождения тестов тестируются всё большие и большие части приложения.

3. Модульное тестирование — тестирование для ленивых. В купе с метриками покрытия строк кода, они позволяют легко и просто пускать пыль в глаза менеджменту: вроде и 100% покрытие, но большая часть функционала оказывается между модулями, а не внутри них, так как отдельные модули при модульном тестировании начинают стремятся делать как можно проще и «тестируемее».
Нет смысла разделять модульные тесты и интеграционные — это лишь частные и сильно ограниченные случаи приёмочных тестов.

А то, что бывают не только приемочные тесты — и при этом «оставшиеся» все равно могут быть как модульными, так и интеграционными, вас не смущает?

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

… ну то есть начинаем мы от хранилища, файловой системы, внешних веб-служб и так далее?

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

А как вам удается разместить функциональность между модулями — т.е., в месте, где нет кода?

Модульное тестирование — тестирование для ленивых.

Я и не спорю, кстати. Я вот ленивый, поэтому предпочитаю модульное тестирование — оно позволяет достичь нужного мне результата проще, с меньшим количеством усилий и существенно быстрее.
А то, что бывают не только приемочные тесты — и при этом «оставшиеся» все равно могут быть как модульными, так и интеграционными, вас не смущает?
Ок, давайте поспорим о терминах. Какие ещё бывают тесты, кроме «проверки соответствия функциональным требованиям»? Лично я не вижу смысла делить тесты иначе чем на «тесты корректности» и «тесты производительности».

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

А как вам удается разместить функциональность между модулями — т.е., в месте, где нет кода?
Ну, это не мне. Это места инициализации модулей, места их соединений, конфиги.

Я и не спорю, кстати. Я вот ленивый, поэтому предпочитаю модульное тестирование — оно позволяет достичь нужного мне результата проще, с меньшим количеством усилий и существенно быстрее.
Быстрее чем что?
Какие ещё бывают тесты, кроме «проверки соответствия функциональным требованиям»?

Регрессионные — т.е., те, которые фиксируют поведение, ранее обнаруженное в багах. В требованиях оно может быть не описано при этом.

Именно так

Вы понимаете, что вы тем самым усложняете тестирование на несколько порядков?

А вот на CI-сервере уже гонять по полной.

CI-сервер не сдохнет, случайно?

Это места инициализации модулей, места их соединений, конфиги.

Так это тоже же тестировать надо.

Быстрее чем что?

Чем интеграционное с использованием БД, файловой системы, сетевого и межпроцессного взаимодействия.
Регрессионные — т.е., те, которые фиксируют поведение, ранее обнаруженное в багах. В требованиях оно может быть не описано при этом.

Не повторять багов — вполне себе требование. Да и само существование бага — невыполнение функционального требования.

Вы понимаете, что вы тем самым усложняете тестирование на несколько порядков?
Наоборот, упрощаю. А так же значительно увеличиваю качество тестирования и своевременность обнаружения проблем на стыке со внешними сервисами.

CI-сервер не сдохнет, случайно?
Нет.

Так это тоже же тестировать надо.
Только это уже не модульные тесты.

Чем интеграционное с использованием БД, файловой системы, сетевого и межпроцессного взаимодействия.
Не стоит впадать в крайности. Как я уже сказал, модульные тесты просты и быстры, но проверяют далеко не всё, и их прохождение не гарантирует, что ваше приложение вообще запустится.
Не повторять багов — вполне себе требование. Да и само существование бага — невыполнение функционального требования.

Я уже поправился ниже: разница не в отношении к требованиям, а в, так скажем, временном аспекте. Регрессионные тесты смотрят «назад», приемочные — «вперед».

Наоборот, упрощаю

То есть необходимость сетапить (в том числе — поддерживать изоляцию и корректное состояние) несколько разнородных зависимостей — это упрощение?

А так же значительно увеличиваю качество тестирования и своевременность обнаружения проблем на стыке со внешними сервисами.

Давайте разберемся. Сначала «проблемы на стыке со внешними сервисами». Вы же не хотите сказать, что вы будете из CI-окружения использовать «боевой» внешний сервис? Наверное, нет. Значит, проблемы на стыке с ним (равно как и проблемы конфигурации боевого окружения и прочие милые вещи) вы получите все равно потом, так что эта «своевременность» — некоторая иллюзия. Вы получаете некоторые проблемы раньше. Вопрос того, какой ценой.

Теперь «качество тестирования». Что вы понимаете под качеством? Потому что я в данном контексте могу выделить следующие варианты: тестовое покрытие, скорость обратной связи, точность обратной связи (место обнаружения ошибки) и, наконец, надежность покрытия (в значении «количество ложных срабатываний в обе стороны»). Аналогично, пойдем по порядку.

Чтобы получить значительный выигрыш в тестовом покрытии, вам надо покрыть как минимум все те сценарии, которые покрываются модульными тестами. А теперь смотрите: вот вы протестировали модуль, который зависит от какого-то внешнего стимула, у него есть столько-то ветвей. Вы все их покрыли. Теперь вы тестируете следующий модуль, который зависит от предыдущего и от внешнего стимула. Теперь вам надо покрывать все стимулы предыдущего модуля и все стимулы этого (чтобы быть уверенным, что они корректно интегрируются). Таким образом, чтобы получить то же самое покрытие, что в модульных тестах, вам надо затратить больше ресурсов (причем во всех смыслах).

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

С точностью обратной связи и количеством ложных срабатываний мне кажется что будет более-менее одинаково в обоих вариантах.

Собственно, то, что вы описываете — это типичный «запашок» под названием «зависимые тесты».

(ну и да, мы еще не затрагивали сложность тестирования исключительных ситуаций в «живых» окружениях)

Нет

Завидую вам. Я таких мощностей в своем распоряжении никогда не имел (и вряд ли буду).

Только это уже не модульные тесты.

Смотря что. Инициализация и места соединений могут быть покрыты модульными тестами. Конфигурация — сложнее, но тоже может.

Не стоит впадать в крайности

А я и не впадаю. Я просто говорю, что для меня, как для ленивого программиста, модульные тесты обычно выгоднее, потому что дают больший эффект при меньших затратах. Иначе говоря, я предпочту иметь один функциональный тест, проверяющий, что приложение запускается и проходит happy path, и тысячи модульных тестов, проверяющих всякие побочные ветви, нежели покрывать это все тысячами функциональных тестов.
Я уже поправился ниже: разница не в отношении к требованиям, а в, так скажем, временном аспекте. Регрессионные тесты смотрят «назад», приемочные — «вперед».
Опять же, нет никакого смысла разделять эти два типа тестов. Просто одни пишутся до деплоя, а другие после, ибо до деплоя по каким-либо причинам не написали.

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

Давайте разберемся. Сначала «проблемы на стыке со внешними сервисами». Вы же не хотите сказать, что вы будете из CI-окружения использовать «боевой» внешний сервис? Наверное, нет.


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

Чтобы получить значительный выигрыш в тестовом покрытии, вам надо покрыть как минимум все те сценарии, которые покрываются модульными тестами. А теперь смотрите: вот вы протестировали модуль, который зависит от какого-то внешнего стимула, у него есть столько-то ветвей. Вы все их покрыли. Теперь вы тестируете следующий модуль, который зависит от предыдущего и от внешнего стимула. Теперь вам надо покрывать все стимулы предыдущего модуля и все стимулы этого (чтобы быть уверенным, что они корректно интегрируются). Таким образом, чтобы получить то же самое покрытие, что в модульных тестах, вам надо затратить больше ресурсов (причем во всех смыслах).
Нет смысла в следующем модуле тестировать предыдущий. То есть тесты тесты для следующего модуля пишутся в предположении, что предыдущий реализован правильно. Таким образом, чтобы получить то же покрытие, что и в модульных тестах, достаточно затратить столько же ресурсов, а то и меньше, ведь не нужно мокать всё подряд. При этом в качестве бонуса мы получаем проверку взаимодействия модулей.

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

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

С точностью обратной связи и количеством ложных срабатываний мне кажется что будет более-менее одинаково в обоих вариантах.
А как же упомянутая в топике проблема несоответствия моков?

Собственно, то, что вы описываете — это типичный «запашок» под названием «зависимые тесты».
А то, что вы описываете — это типичный «запашок» под названием «тесты сферического модуля в вакууме».

(ну и да, мы еще не затрагивали сложность тестирования исключительных ситуаций в «живых» окружениях)
А в чём там сложность?

Завидую вам. Я таких мощностей в своем распоряжении никогда не имел (и вряд ли буду).
Какими «такими»?

Смотря что. Инициализация и места соединений могут быть покрыты модульными тестами. Конфигурация — сложнее, но тоже может.
Покрывать места соединения модульными тестами — равносильно кратному увеличению мест соединений. То есть, если вы изолированно протестировали модуль А и модуль Б, а потом протестировали изолированную связь АБ, то у вас образовалось ещё 2 непротестированные связи: А-АБ и и АБ-Б.

А я и не впадаю. Я просто говорю, что для меня, как для ленивого программиста, модульные тесты обычно выгоднее, потому что дают больший эффект при меньших затратах. Иначе говоря, я предпочту иметь один функциональный тест, проверяющий, что приложение запускается и проходит happy path, и тысячи модульных тестов, проверяющих всякие побочные ветви, нежели покрывать это все тысячами функциональных тестов.
Ну, это ещё ничего, некоторые вообще тесты писать ленятся :-D
Опять же, нет никакого смысла разделять эти два типа тестов.

Есть. У них разный импакт.

Да, это упрощение автоматической всесторонней проверки продукта.

Упрощение по сравнению с чем?

Вообще-то да.

Всегда?

Простейший пример: авторизация через соцсеточки. Отдельный тестовый сервер соц сети не предоставляют, да и смысла в этом мало,

А вот ЕСИА, например, наоборот, обязывает интегрирующиеся системы работать с тестовым экземпляром.

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

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

Вы разбиваете на модульные и интеграционные (половину кейсов проверяем сейчас, половину потом),

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

… который начнётся не с самого начала, а с изменённого модуля.

Тогда вы не будете уверены, что предыдущий код работает. Так делать не надо.

А как же упомянутая в топике проблема несоответствия моков?

Я с ней встречаюсь редко.

А то, что вы описываете — это типичный «запашок» под названием «тесты сферического модуля в вакууме».

В какой литературе он описан?

А в чём там сложность?

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

Какими «такими»?

Ну так, чтобы проверить «типичную» современную систему, нужно иметь два БД-сервера, два сервера приложений, два веб-сервера и две клиентских машины. А теперь помножим на количество одновременно идущих билдов.

Покрывать места соединения модульными тестами — равносильно кратному увеличению мест соединений. То есть, если вы изолированно протестировали модуль А и модуль Б, а потом протестировали изолированную связь АБ, то у вас образовалось ещё 2 непротестированные связи: А-АБ и и АБ-Б.

Не-а. Место «соединения» модулей А и Б — это конкретная точка внутри А, где тот вызывает Б. Когда мы подменяем в тесте Б, мы знаем, что А вызывает его корректно. И наоборот. Остается протестировать инициализацию — т.е. то, что во время «боевой» сборки системы А получает нужный Б.

Ну, это ещё ничего,

Это не просто «ничего», это еще и (проверенно) быстрее по времени отклика, чем у соседа, который пишет функциональные тесты на каждый чих.
На самом деле, я не совсем прав по поводу регрессионных тестов. Более верно разделять регрессионные и приемочные тесты следующим образом: приемочные тесты фиксируют новую (или изменяемую) функциональность, регрессионные тесты фиксируют ранее существовавшую функциональность. В принципе, при смене итерации приемочные тесты даже могут стать регрессионными — но могут и нет.
Полностью поддерживаю автора.
Использование аля интеграционных тестов вместо классических unit
Экономит много больше времени. чем поиск ошибок когда покраснело несколько тестов.
во первых вы пишите тесты на один класс и косвенно тестируете еще множество классов.
во вторых вы не тратите время на написание и отладку моков, стабов, и тп.
Исходя из практике ломаются как правило не отдельные классы а «стыки» то есть то самое что интеграционные тесты и тестируют.

И чем мельче и проще модули, тем больше логики находится между ними.

во первых вы пишите тесты на один класс и косвенно тестируете еще множество классов.

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

Допустим есть два модуля: А и В. В зависит от А. Пишем тесты на А и тесты на В использующего А. Видим падение в В. Есть два варианта:


  1. В плохо написан. Мы поймали ошибку в том классе, который тестировали, всё ок.
  2. А плохо написан и плохо протестирован, а с В всё в порядке. Если бы В не использовал А, то все тесты бы прошли, не поймав ошибку.

Вопрос: какая из двух ситуаций хуже?


  1. Ошибка не обнаружена.
  2. Ошибка обнаружена, но требует работы по локализации.

Ну так не надо использовать unit-тесты вместо интеграционных.

Во-первых, потому, что правильную "сборку" системы тоже надо проверить.
Во-вторых, потому что общие поведенческие тесты добавляют уверенности в том, что конкретные сценарии работают целиком.

Вы не ответили зачем разделять тесты на интеграционные и модульные.

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

Не зависимости делают тест медленным.

Во-первых, обычно как раз зависимости: потому что первые зависимости в очередь на подмену — это БД, файловая система, сеть и так далее. На несколько порядков медленнее, чем то, что делает сам код (кроме суровых алгоритмов, но у них вообще все по своему).


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


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


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


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


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


"Честные" интеграционные тесты, затрагивающие тот же компонент, намного сложнее писать и развертывать, и они идут десятки минут (весь пакет).


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

С необходимостью мокать внешний мир никто не спорит. Но из неё не следует необходимость тестирования каждого класса в отрыве от остальных.

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

Oно конечно так. Но мой опыт говорит, что в отдельном классе даже тестировать зачастую нечего и классов таких достаточно много. Вместе они образуют некий Unit который я и тестирую
При этом я не мокую базу ибо это долго и в результате я получаю один тест (testCase возможно много), да он ходит относительно долго но его быстро написать и он более или менее корректный и скорее покажет что возникла ошибка.
Кроме того он не устареет если к примеру изменится название поля в базе или любое другое взаимодействие внутри моего Unit.
Когда я пробовал мокать базу я потратил на написание теста много больше времени и все равно вынужден был писать тест на взаимодействие с базой.
Писать тесты на каждый класс на самом деле безумно долго Тесты будут зачастую простые до идиотизма. и любое изменение потребует переписать кучу тестов.
А общий интеграционный тест все равно необходим.
Но мой опыт говорит, что в отдельном классе даже тестировать зачастую нечего и классов таких достаточно много.

Ну так не тестируйте, никто ж не заставляет.


Вместе они образуют некий Unit который я и тестирую

А вот это уже странно. Много классов, в которых тестировать нечего, вместе не могу образовывать нечто, где есть, что тестировать. Ну или вы умалчиваете, что у вас есть что-то, что объединяет эти классы, что вы на самом деле и тестируете.


При этом я не мокую базу ибо это долго

Почему долго-то? В коде с корректно разделенными ответственностями это быстро.


да он ходит относительно долго но его быстро написать

Быстро ли? У меня есть жизненный пример, когда интеграционные тесты пишутся намного медленнее, чем юнит (именно потому, что надо много всего учитывать).


скорее покажет что возникла ошибка.

Почему "скорее"? Выполняется он медленнее, и вариантов тоже покрывает меньше.


Кроме того он не устареет если к примеру изменится название поля в базе

Правда? А откуда у вас берется БД с актуальными названиями полей (да еще и доступная с CI-сервера)? А представьте, что у вас одновременно пошли тесты по трем версиям, у каждой из которых своя БД?


И да, мои юнит-тесты тоже не устаревают, когда меняется название поля в БД. Им вообще плевать на названия полей в БД.


Писать тесты на каждый класс на самом деле безумно долго

Не надо писать на каждый. Надо писать на те, где это оправданно.


Тесты будут зачастую простые до идиотизма

Это же прекрасно. Простой тест — меньше ошибок в самом тесте, меньше ложных срабатываний, проще сетап.


и любое изменение потребует переписать кучу тестов.

Почему вдруг?


А общий интеграционный тест все равно необходим.

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

Но мой опыт говорит, что в отдельном классе даже тестировать зачастую нечего и классов таких достаточно много.

Ну так не тестируйте, никто ж не заставляет.

Да не заставляет, но концепция UnitTest именно в том, что бы тестировать классы по отдельности.

Вместе они образуют некий Unit который я и тестирую

А вот это уже странно.
Много классов, в которых тестировать нечего, вместе не могут образовывать нечто, где есть, что тестировать.
Ну или вы умалчиваете, что у вас есть что-то, что объединяет эти классы, что вы на самом деле и тестируете.

Тут уже словоблудие, естевственно я тестирую именно взаимодействие классов.

При этом я не мокую базу ибо это долго

Почему долго-то? В коде с корректно разделенными ответственностями это быстро.

Потому что наш код тесно переплетен с базой.
Все объекты берут свои метаданные из базы и… в общем случае корректно создать класс без обращения к базе весьма и весьма долгона
На самом деле я давно думаю написать какую ни будь заглушку, что бы кешировать обращение к базе.

да он ходит относительно долго но его быстро написать

Быстро ли? У меня есть жизненный пример, когда интеграционные тесты пишутся намного медленнее, чем юнит (именно потому, что надо много всего учитывать).

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

скорее покажет что возникла ошибка.

Почему «скорее»? Выполняется он медленнее, и вариантов тоже покрывает меньше.

Мне не критично сколько по времени ходят тесты на сервере непрерывнои интеграции. А вот вероятность поймать ошибку выше потому, что косвенным образом тестируют еще много чего.

Кроме того он не устареет если к примеру изменится название поля в базе

Правда? А откуда у вас берется БД с актуальными названиями полей (да еще и доступная с CI-сервера)?
А представьте, что у вас одновременно пошли тесты по трем версиям, у каждой из которых своя БД?

Ну естевственно нужная база должна быть доступна с сервера. И у каждой версии своя база.

И да, мои юнит-тесты тоже не устаревают, когда меняется название поля в БД. Им вообще плевать на названия полей в БД.

Но вы все равно вынуждены писать тесты на корректную работу с базой.

Писать тесты на каждый класс на самом деле безумно долго

Не надо писать на каждый. Надо писать на те, где это оправданно.

Что я и делаю.

Тесты будут зачастую простые до идиотизма

Это же прекрасно. Простой тест — меньше ошибок в самом тесте, меньше ложных срабатываний, проще сетап.

Ответ в вы сами дали в пункте выше.

и любое изменение потребует переписать кучу тестов.

Почему вдруг?

Изменив один класс я должен переписать все моки этого класса. И хорошо еще если ни чего не забуду.

А общий интеграционный тест все равно необходим.

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

Как я уже говорил время хождения тестов не столь критично.

РЕЗЮМЕ:
Писать тесты нужно исходя из критерия СТОИМОСТЬ-ЭФФЕКТИВНОСТЬ.
Стомость-время затраченное на написание (Поддержку).
Эффективность — шанс найти ошибки.
И я нахожу, что по этому критерию большая часть должна быть именно интеграционных тестов.

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

Чем короче цикл «написал код — запустил тест — получил результат», тем больше мотивация включать в тесты новые случаи.

Если же тест выполняется 20 минут, то напишут «для галочки» пару сценариев, и обновлять будут раз в полгода, а то и никогда, т.е. вынужденно, в случаях, когда тест падает из-за изменений логики. Разработка не по принципу «новые требования — меняем тесты — меняем код», а по принципу «новые требования — меняем код — меняем тесты, которые упали».

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


Это значит, что во время разработки программисты у вас не пользуются тестами.

Увы во всей компании только я один использую тесты
Тесты разбиты по категориям 'дневной' и 'ночной' и TeemCity гоняет их после каждого комита (не только моего) дневные тесты ходят обычно минут 5 и это сопоставимо с временем сборки всех проектов.
А на своем компе я гоняю обычно тесты того что я разрабатываю и опять время выполнения тестов не на много больше времени сборки. Так что быстродействие тестов пока для меня не критично.
концепция UnitTest именно в том, что бы тестировать классы по отдельности.

Нигде же не сказано, что все, правда?


естевственно я тестирую именно взаимодействие классов.

Не, подождите. Взаимодействие — это всего лишь набор взаимных действий. Если у вас в классе нечего тестировать, значит, он не совершает значимых действий (например, это DTO), значит, его вклад во взаимодействие нулевой; если это применимо к каждому классу, значит и общий набор тоже нулевой.


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


Потому что наш код тесно переплетен с базой.
Все объекты берут свои метаданные из базы

Так это и есть плохое разделение ответственностей. За взаимодействие с БД должен отвечать отдельный слой, за метаданные — поставщик метаданных, и так далее.


Однако общее время написания одного интеграционного теста все равно в разы меньше, чем время написания Unit на каждый класс внего входящий, а потом еще интеграционный тест.

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


Мне не критично сколько по времени ходят тесты на сервере непрерывнои интеграции.

То есть локально вы тесты не выполняете? Времяпрепровождение формата "а теперь давайте запустим набор интеграционных тестов и сорок минут покурим" вам незнакомо?


А вот вероятность поймать ошибку выше потому, что косвенным образом тестируют еще много чего.

Вы вероятность считаете на один запуск теста или на минуту выполнения?


И нет, опыт показал, что интеграционные тесты, конечно, "тестируют еще много чего", но не дают никаких гарантий, что это "много чего" адекватно покрыто тестами.


Ну естевственно нужная база должна быть доступна с сервера. И у каждой версии своя база.

И все эти БД надо поддерживать в актуальном состоянии. Это место, это ресурсы.


Но вы все равно вынуждены писать тесты на корректную работу с базой.

Во-первых, если за работу с БД отвечает не мой компонент, я могу вообще не писать на него тесты.


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


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


_Надо писать [тесты] на те [классы], где это оправданно.
Что я и делаю.

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


Изменив один класс я должен переписать все моки этого класса.

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


Как я уже говорил время хождения тестов не столь критично.

Правда? Мне вот не нравится, что изменение я закоммитил сегодня, а не валит ли оно систему я узнаю завтра. Особенно мне это не нравится, когда мне нужно делать следующее изменение, зависящее от этого.


Увеличение времени тестирования — это увеличение времени цикла разработки, а оно приводит к каскадному увеличению сроков выпуска новой версии. Если у вас цикл тестирования занимает день, сколько изменений вы можете внести за неделю?


Писать тесты нужно исходя из критерия СТОИМОСТЬ-ЭФФЕКТИВНОСТЬ.
Стомость-время затраченное на написание (Поддержку).
Эффективность — шанс найти ошибки.

Вы упускаете один важный момент: эффективность — это шанс найти ошибки за заданное время.

Увеличение времени тестирования — это увеличение времени цикла разработки, а оно приводит к каскадному увеличению сроков выпуска новой версии.
Либо есть вариант не тестировать )))

Дискуссионный вопрос: SOLID удешевляет продукт или нет? Вся эта декомпозиция, разделение отвественности, юнит-тестирование отнимают время и не факт, что окупаются.

Рассмотрим сферический в вакууме пример. Сервис приёма заказов должен давать скидку 20%, если дата заказа попадает на день рождения клиента.

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

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

Если посмотреть со стороны, он молодец — таск закрыл за 10 минут. А посмотреть на того, кто делал по SOLID — он 2 часа ковырялся. А бизнесу важно что? Лишь бы сейчас побыстрее и подешевле. Вот такие размышления…
Дискуссионный вопрос: SOLID удешевляет продукт или нет?

Да чего в нем дискуссионного — взяли и померяли стоимость поддержки в одном и другом случае.


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

Это как раз переусложнение. YAGNI.


Обычный же программист (не перфекционист) просто добавит умножение на 0.8 суммы заказа где-то внутри сервиса.

Ну да, и написать снаружи еще один юнит-тест на этот же сервис, который подставит нужного клиента.


А бизнесу важно что? Лишь бы сейчас побыстрее и подешевле.

Бизнес — он разный бывает.

взяли и померяли стоимость поддержки в одном и другом случае
Обычно это невозможно. Нельзя иметь полностью одинаковые команды, отличающиеся только подходом к декомпозиции и тестированию. Нужно много команд для хорошей статистики.
Это как раз переусложнение. YAGNI.
Не смущает, что теперь у класса две ответственности: создание заказа и вычисление скидки, а юнит-тесты проверяют сразу две фичи? (если ломается, например, создание, может упасть тест, отвечающий за скидку).
Нельзя иметь полностью одинаковые команды, отличающиеся только подходом к декомпозиции и тестированию. Нужно много команд для хорошей статистики.

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


Не смущает, что теперь у класса две ответственности: создание заказа и вычисление скидки

Не, не смущает. Переусложнять не надо.


а юнит-тесты проверяют сразу две фичи? (если ломается, например, создание, может упасть тест, отвечающий за скидку).

Тоже не смущает. Если сломается создание, должен упасть тест, отвечающий за создание, с него и надо начинать.


Вот если эта проблема станет регулярной — тогда можно подумать о том, чтобы разнести тесты.

Не, не смущает
С таким подходом мы никогда не вынесем логику скидок в отдельный класс. Ведь когда уже написано N правил, хуже не станет, если допишем (N+1)-е и в очередной раз исправим все тесты создания заказа.
Ведь когда уже написано N правил, хуже не станет, если допишем (N+1)-е и в очередной раз исправим все тесты создания заказа.

Вот как раз в тот момент, когда для дописывания одного правила нужно исправлять много тестов, и становится понятно, что пора рефакторить.

я для себя сделал такое определение Unit для юнит теста это класс или группа классов имеющих какое то законченное поведение.
И в тесте я проверяю именно соответствие этому поведению.
Про взаимодействие с базой.
Естественно взаимодействие с базой у нас через интерфейсы и при желании их можно подменить но
  1. При любом раскладе я должен проверить корректность «Маппинга» полей в базе на свойства класса
  2. Откуда брать корректурные значения для заполнения свойств классов?
  3. То же самое и про метаданные

И учитывая, что тестов пока не запредельно много я не мокаю базу.
я для себя сделал такое определение Unit для юнит теста это класс или группа классов имеющих какое то законченное поведение.

Согласно этому определению вся система — тоже юнит.


Но не суть.


И в тесте я проверяю именно соответствие этому поведению.

Вы же говорили, что предпочитаете интеграционные тесты, так какое отношение к этому имеет ваше определение юнит-тестов?


При любом раскладе я должен проверить корректность «Маппинга» полей в базе на свойства класса

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


Откуда брать корректурные значения для заполнения свойств классов?
То же самое и про метаданные

Оттуда же, откуда и значения для заполнения БД — из тест-кейса.


И учитывая, что тестов пока не запредельно много я не мокаю базу.

Вот в этом и проблема интеграционных тестов — они плохо масштабируются. Мне вот нужно очень много тестов.

1) Да система то же Unit (только очень большой)
2) Да я для себя считаю все тесты Unit хотя на самом деле почти все мои тесты интеграционные.
3)Базу я не мокаю потому, что по очень долго заполнять все свойства из параметров теста и прочее
реально встречаются очень сложные объекты с десятками свойств и вложенных свойств.
их в моем конкретном случае проще начитать из базы.
Вообще где то в планах сделать для теста кешируемое обращение к базе.
Да система то же Unit (только очень большой)

Извините, но это противоречит общепринятому пониманию юнит-тестов. Так что нет.


Да я для себя считаю все тесты Unit хотя на самом деле почти все мои тесты интеграционные.

А какой смысл считать для себя тесты юнит-тестами, если они интеграционные? И какой смысл говорить, что вы считаете, что интеграционных тестов должно быть больше?


Да я для себя считаю все тесты Unit хотя на самом деле почти все мои тесты интеграционные.

А базу заполнять быстро, да? Или вы базу, на самом деле, не заполняете?

1)Смысл в том что мои «считалки» идут в разрез с общей терминологией. И общая практика вроде бы как раз писать больше чистых Unit тестов.
2) Наши базы это эталонные базы соответствующих версий. Так как я тесты провожу в одной транзакции с последующим откатом, то насрать туда нереально.
Единственный мой мок базы в том, что я использую всегда один коннект и начинаю транзакцию перед тестом.
(Система построена так, что все внутренние транзакции в таком случае создают savePoint)

Давайте еще раз взглянем на это на конкретном примере.


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


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


Сначала у нас появляются требования, которые нельзя верифицировать через публичный интерфейс: например, сервис должен отдавать какие-то справочные данные, которые через него нельзя никак добавить/поменять. Или, наоборот, сервис должен менять какие-то данные (например, аудит операций), которые через него нельзя получить. Для первого случая нам приходится писать согласованный с тестом сетап (т.е. тест должен знать, какие данные мы засетапили), для второго — верификацию другими средствами (например, через БД). В обоих случаях мы внесли в тест зависимость не только от тестируемого кода, но и от БД — и конечно же, теперь при изменении БД тест надо править.


Дальше — больше: внезапно выясняется, что с одним из клиентов сервис работает неправильно (SOAP и XML, конечно, стандарты, но...). Надо добавить тесты на то, что сообщения, отправляемые таким клиентом, понимаются сервисом корректно. Но сервис, вот беда, не выставляет способа проверки, корректно ли он понял сообщение, единственный способ это узнать — это послать ему бизнесовое сообщение и проверить, правильный ли бизнес-результат. Это приводит к тому, что вы вынуждены выделить из ваших поведенческих тестов выше тот набор, который покрывает репрезентативное множество входящих сообщений, и продублировать его, заменив отправку сообщений со стандартной на "проблемную". Пять разных проблемных клиентов — пять дублей. Вы, конечно, как можете, выносите общие части тестов, но это все равно разрастается совершенно бесконтрольно.


И тут приходит бизнес и говорит: а нам нужно, чтобы у этого сервиса был REST-эндпойнт. Поведение то же самое, только через REST. И что? Вам опять надо скопировать все тесты (причем для разных вариантов отправляемых запросов).


А теперь давайте представим, что у вас сервис разбит всего лишь на две части: собственно сервисный эндпойнт и бизнес-объект. Мы даже не будем обсуждать, как тестировать сам бизнес-объект, предположим, что его можно тестировать только интеграционно.


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


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


Дальше мы повторяем эту процедуру для других представлений, для REST и так далее, далее, далее, получая профит на каждой итерации.


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

У каждого естественно своя ситуация и он сам может определить для себя СТОИМОСТЬ-ЭФФЕКТИВНОСТЬ.

… поэтому давайте сделаем вид, что общепринятых практик не существует.

Sign up to leave a comment.

Articles