Pull to refresh

Comments 69

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

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

Интеграционные тесты переписывать не пришлось вообще. Чему я был рад до безумия.

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

А общение с компонентами через websockets это какие тесты — unit, интеграционные, rкомпонентные, end-to-end или еще какие?

Это странно. Компоненты перебрасываются какими то JSON сообщениями. Какая разница, по какому протоколу. Юнит тестам должно быть все равно.

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

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

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

Мы сейчас как раз плавно переезжаем с интеграционных тестов на юнит так как 80% времени всего кодинга народ у нас чинил интеграционные тесты.

Интеграционные тесты хороши, но в меру. Они должны тестировать интеграцию компонентов, а не их логику. Для логики есть юнит тесты.
Полностью поддерживаю! Думал, я крамолу несу, а оказалось, не я один :)

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

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

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

Собственно, сейчас мы умудряемся на интеграционных сценариях даже работать в парадигме test-first, и это дает очень хорошие результаты.
А зачем юнит-тест переписывать? У любого теста есть жизненный цикл. Иногда тест дешевле удалить и написать новый, а не править текущий под новые реалии.
Из текста статьи (и из ряда комментариев), создается впечатление, что, для многих, интеграционные тесты приравниваются к Sociable Unit Tests, а юнит-тестами считаются исключительно полностью изолированные Solitary Unit Tests. Я, конечно, могу в этом ошибаться, но мне так показалось. В таком случае, хотелось бы привести слова основателя TDD Кент Бека: "My personal practice — I mock almost nothing.". Интеграционные и юнит-тесты имеют немного разные цели.

можно менять интерфейс взаимодействия внутренних компонентов

Самотестируемость кода является первостепенным условием для осуществления его рефакторинга. А поэтому, действительно, тесты должны облегчать рефакторинг, а не накладывать на код оковы. Тестировать нужно поведение, а не реализацию, и спускаться в глубь реализации следует тогда, когда это необходимо для сокращения количества комбинаций тестовых условий. Наглядный пример: «Many people make bad trade-offs, especially with heavy mocking. Kent thinks it’s about trade-offs: is it worth making intermediate results testable? He used the example of a compiler where an intermediate parse-tree makes a good test point, and is also a better design.» — "Is TDD Dead?"

P.S.: Раз уж статья была помечена тэгом ТДД, то хотелось бы обратить внимание, что ТДД — это не методика тестирования, а методика проектирования и разработки.

"Sociable Unit Tests" — не являются юнит тестами по определению, так как тестируют не один модуль, а сразу группу модулей. Такие тесты называются компонентными.

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

По определению юнита — куска кода.
Для стандартной библиотеки обычно делается оговорка. Что лишний раз подчёркивает глупость понятия "модульный тест".

Тогда что такое "тестировать X". Верно ли что если что-то тестирует X он должен выполнять только X?

Ну давайте и e2e тесты модульными называть тогда, что уж там.

Есть ли еще варианты? Можно ли придумать какой-то другой принцип называть что-то "тестированием X", который одновременно будет позволять использовать не только X при этом не называть E2E тест модульным?

Я надеюсь, что вы, все-таки, прошли по ссылке, и ознакомились, как минимум, с названием статьи. То, что тестируемый вами юнит взаимодействует с другими, вовсе не означает то, что вы тестируете другие юниты:

«But not all unit testers use solitary unit tests

«Indeed using sociable unit tests was one of the reasons we were criticized for our use of the term „unit testing“. I think that the term „unit testing“ is appropriate because these tests are tests of the behavior of a single unit. We write the tests assuming everything other than that unit is working correctly.»

Вы лучше свой головой подумайте, а не молитесь на священные писания.


  1. Вы пишите тест используя апи одного модуля.
  2. Ошибка во втором модуле может завалить ваш тест.
  3. Следовательно вы тестируете оба модуля.

Это элементарная логика.

А из чего следует, что если заваливается тест Т при при испорченном X то это является тестом X?


Например, при покупке лампочки в магазине вы говорите "можно я проверю лампочку?", а не "можно я проверю, лампочку, цолоколь, провода, подстанцию, электростанцию и водохранилище"?

Потому что результат теста T зависит от X.


Лампочку вы проверяете используя тестовый стенд, который изолирует лампочку от вашего торшера, вашей электросети и вашей ТЭЦ. Вообще, аналогии из физического мира тут не к месту.

"Вы лучше свой головой подумайте"


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


Вообще, аналогии из физического мира тут не к месту.

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


Может быть разберемся почему?


Например, на основании какого принципа вы называете тестированием именно лампочки процесс который даст сбой при отказе ТЭЦ? Почему вы его не называете тестированием ТЭЦ?

Как он изолирует от моей ТЭЦ?
Купить дизель-генератор для проверки лампочки :)

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

P.S.: Пример с лампочкой был очень удачным, спасибо.
Купить дизель-генератор для проверки лампочки :)

Не поможет — в софтверной вселенной vintage это станет тестом дизель-генератора и лампочки.


Объектом тестирования выступает поведение, а не юнит.

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

Тестовый стенд не является объектом тестирования. Зависимости модуля не являются тестовым стендом (если они не предоставлены самим стендом, разумеется).


Завязывайте уже с этой софистикой, она ни к чем хорошему вас не приведёт.

То есть важно не то, что "ошибка во втором модуле может завалить ваш тест." а то, что является объектом тестирования?


Я могу для тестирования лампочки использовать экземпляр той же модели цоколя, что я использую в своем торшере?

  1. Всё не так однозначно. Обычно цель тестирования модуля — проверить, что его поведение не изменилось. Если тест не падает, то оно не изменилось (предполагаем, что тест действительно тестирует зафиксированное поведение. Если падает, то есть варианты:
    • изменилось поведение нашего модуля — тест выполнил не тольео свою основную задачу, но и помог локализовать ошибку
    • изменилось поведение зависимости нашего модуля и тест упал — это может быть отрицательное срабатывание нашего теста, а может быть ложноотрицательное. Если эта зависимость нигде больше никем не используется, если она в приложении нужна только нашему модулю, то изменение её поведения равносильно изменению поведения нашего модуля и тест выполнил свою основную задачу: выявил изменение поведение.

Не понял вашу мысль. Вы описали, что компонентные тесты — это хорошо. Ну как бы да, об этом и речь.

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


Допустим у нас есть модули M1, M2 и M3. M2 и M3 реализуют интерфейс I. M1 используют интерфейс I.


В проде M1 всегда получает M2. В тесте мы передаем ему M3.
Тест сформулирован в терминах требований к M1.


Это в вашей системе терминологии:


  1. Компонентный тест (если да, то для какого компонента чего)?
  2. Модульный тест?
  3. Интеграционный тест?
В проде M1 всегда получает M2. В тесте мы передаем ему M3.

В тесте мы передаём M2. Кроме некоторых исключительных случаев.


в вашей системе терминологии

Это не моя терминология.

В тесте мы передаём M2

А если передаем M3? В некотором исключительном случае.


в вашей системе терминологии
Это не моя терминология.

Ок — в истинной системе терминологии, как ее понимают вы и другие люди, которые думают своей головой а не фанатики, читающие священные книги. Это какой тест?

UFO just landed and posted this here

Тест должен давать понятный результат, поэтому я не использую контрольную сумму. Я использую либо проверку конкретных свойств объекта, либо сравнение на тот же самый экземпляр, либо структурную эквивалентность.

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

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

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

А если организовать все так, что в логе будет написано?

Так это и есть самый простой способ организовать. Его легче всего читать глазами, все везде будет написано явно и мы точно знаем, если где-то что-то пропущено. Хелперы в тестах вообще раздражают. Кроме того, в реальной ситуации помимо равенства точного бывает мягкое, когда часть полей можно проигнорировать, часть contains, часть equalsIgnorcase, часть nullable, а в тесте можно точно написать, что мы ждём.

Реальные ситуации бывают разные. В каких-то случаях проще сравнить весь объект. Например сортировку каких-то списков или копирование удобно сравнивать целиком.


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

В общем, я просто всегда имею в виду возможный misuse этого способа джуниорами.
Из моего опыта bulk методы сравнения часто приводят к тому, что, во-первых, объект уже перегружен свойствами, т.е. сама потребность в балк-методе — это smell.
Так же при появлении новых свойств у класса они начинают автоматически появляться в объектах и проверяться. С позиции читателя предпочтительно, чтобы тест упал и свойство было в явном виде добавлено.
Насчет коллекций труднее, там вообще трудно с ассертами, слишком много утверждений приходится подтверждать\опровергать зараз и слишком много искушений обобщить, что приводит к труднопонимаемым тестам. Я не видел какого-то реалистичного консистентного подхода пока и стараюсь избегать их. Но к мыслям по улучшению я открыт.
UFO just landed and posted this here

Профессор, а какой ответ правильный-то? Какую методичку почитать и в каком месте?

Не обращайте внимания, это больной)

UFO just landed and posted this here

То есть вы сами не читали методички, а нам двойки ставите? Какая логика стоит за этим?


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

UFO just landed and posted this here

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

Мне и не нужно знать всех вариантов. Мне нужно знать ограниченный набор вариантов, которые использует мой код, для этого существуют тесты для зависимостей. «Реальность» реальных систем преувеличена. Я напишу об этом отдельно.

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

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

не могли бы вы привести пример, в котором интеграционный тест полнее покрывает систему, чем юнит тест?

По ссылке выше очень длинный текст. Непонятно, к чему адресоваться.

Уж не поленитесь и прочитайте его полностью.

Главные преимущества юнит-тестов:


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

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


Конечно, если это хороший код и хорошие тесты

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

martinfowler.com/bliki/TestDouble.html
Еще бы кто-нибудь рассказал, зачем противопоставлять модульные и интеграционные тесты.
И почему в зависимости от решаемых задач нельзя использовать модульные и/или интеграционные тесты.
Так сие не означает, что «используя модульные, нельзя использовать интеграционные; используя интеграционные, нельзя использовать модульные».

Запросто пользуй то, что приносит пользу.

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

Ну да, я про это же. Уточнения:
Там не про порядок, а про количество — чем выше к вершине пирамиды тем меньше тестов.

Количество тестов не является же самоцелью. Цель — доля покрытия логики.
Другое дело, что для повышения покрытия увеличиваем количество тестов. Но это уже следствие.

Цель тестов в идеале — покрыть всю логику. Но есть ограничения как на ресурсы, которые могут быть начально инвестированы собственно в разработку тестов, так и на те, которые могут быть выделены на прогон и поддержку тестов. Пирамида тестирования — компромисс для среднего проекта. В идеале, наверное, квадрат должен быть со 100% покрытием.

Мы обсуждаем что есть тестовая пирамида, а не какова ее цель, например:


Stick to the pyramid shape to come up with a healthy, fast and maintainable test suite: Write lots of small and fast unit tests. Write some more coarse-grained tests and very few high-level tests that test your application from end to end. Watch out that you don't end up with a test ice-cream cone that will be a nightmare to maintain and takes way too long to run.

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

  • Юнит тесты покрывают всю логику
  • Интеграционные тесты появляются тогда, когда юнит тесты написаны ненадежно

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

И другой пример. Написано 2 приложения Сервер и Клиент.
Для них будет 100% интеграционный тест 2-х запущенных приложений и он так же необходим как и юнит тесты, это просто другой уровень.

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

Больше похоже на системное тестирование или вообще e2e. достаточно большая размытость терминов, по-моему, из-за размытости термина "юнит"

Если юнит использует множество других юнитов, то мы их рефакторим и мокаем.
Если у нас клиент и сервер, мы берём тестовый payload и посылаем его клиенту или серверу с соответствующими проверками.

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

Sign up to leave a comment.

Articles