Здравствуйте, меня зовут Дмитрий Карловский и я… люблю гнать всякую странную дичь. Осторожно, после этого доклада у вас может появиться странное, но непреодолимое желание удалить все модульные и e2e тесты из вашего проекта, ибо они требуют много ресурсов, но дают мало профита.  Это — расшифровка выступления на TechLead Conf 2020. Вы можете посмотреть видео, прочитать как статью или открыть в интерфейсе проведения презентаций.  Чем раньше запускать тесты, чем короче цикл отладки и меньше негатива от сопричастных. А чем меньше цикл, тем он быстрее, а значит и поставка новых фич происходит раньше. Пользователи счастливы, что у них ничего не падает. Начальство может точнее планировать сроки. А коллеги уверены, что вы пишете код без багов, хоть на самом деле их и полно, но просто до ревью они не доживают.  Чем раньше вы напишите автоматические тесты, тем больше вы сэкономите времени на ручном тестировании.  Хороший тест указывает на конкретное проблемное место. В языке D, например, есть классная фича — возможность написать тест прямо рядом с кодом. А потом из этого кода генерируется документация, где тесты приводятся как примеры кода. Проверяют всё важное, но не фиксируют неважное.  В часах, например, нам важно, чтобы они показывали точное время, но нам всё равно как именно соединены в них шестерёнки и есть ли там шестерёнки вообще.  Их минимальное возможное число, чтобы охватить все необходимые тестовые сценарии.  Исполняются быстро, а писать просто. Давайте синхронизируем терминологию, чтобы далее говорить на одном языке. Система — единица поставки, ваш продукт. Это то, что вы деплоите на сервер, запускаете на машине пользователя и тп.  Тест системы называется "системным тестом" или "E2E тестом". Модуль — единица кода. Как правило это файл или даже часть этого файла.  Когда вы изолируете кусок кода и пишете для него тест, то такой тест называется "модульным тестом" или "юнит тестом". Компонент — единица функциональности. Он состоит из модуля, являющегося точкой входа, и всех его зависимостей.  Тест компонента называется "компонентным тестом". Можно так же встретить термин "социальный модульный тест", но это какой оксюморон, так как компонентный тест — это частный случай интеграционного тестирования группы модулей, а не одного модуля. Рассказывая про типы тестирования, обычно показывают негативную ситуацию, где мало времени тратится на модульные тесты, но много на ручные.  Это мало того, что медленно. Так ещё и из-за человеческого фактора пропускает много дефектов. Рожок обычно предлагают перевернуть для получения пирамидки, где долгое и сложное тестирование минимизировано, а простое и быстрое максимизировано.  В реальных проектах зачастую получается нечто среднее, вобравшее все минусы из обоих подходов.  Разработчики пишут модульные тесты. На интеграционные их запала уже не хватает. Автотестеры пишут системные тесты с сильной задержкой. А релизить надо уже вчера, поэтому обстоятельное ручное тестирование каждого релиза никуда не уходит. Возьмём простую функцию. Она абстрагирует потребителя от своей внутренней сложности и, в частности, от своих зависимостей. Но чтобы эти зависимости замокать приходится выносить их в публичный интерфейс, выворачивая кишками наружу. Модульные тесты оказывают довольно сильное (и далеко не всегда положительное) влияние на архитектуру и собственно код.  Вы изменили интерфейс модуля B. Теперь вам надо не только изменить условно 10 его тестов, но и 10 моков в модульных тестах зависимых модулей. И соответственно по 10 тестов самих этих модулей, хотя их контракт ничуть не изменился. Поэтому можно сформулировать критерий бесполезности теста. Если: То это бесполезный тест, он фиксирует не важное поведение и будет часто ломаться. И наоборот, если: То этот тест не проверяет то, что должен, что ещё хуже. Чем меньше и проще модули, тем более важно их взаимодействие. Поэтому нужно написать ещё как минимум столько же интеграционных тестов, чтобы проверить именно взаимодействие.  По мере роста приложения растёт не только число тестов, но и время запуска приложения, что даёт квадратичную зависимость времени прогона всех тестов от сложности приложения. То есть очень легко можно достигнуть ситуации, когда прогон системных тестов занимает несколько дней на ферме серверов.  Так, эту кнопку протестировали, сажаем самолёт, и на новый заход для тестирования следующей.  Падение какой-то мелочи с краю может запросто обрушить вам половину тестов. И иди ищи потом кто же виноват. Они поднимают несколько модулей. И выполняют реальную работу, что может быть медленно. Кроме того, беда может быть в вашем тестовом фреймворке. Например, однажды мы обратили внимание, что любой, даже самый тривиальный тест компонента в Ангуляре занимает 100мс. Стали разбираться и выяснили, что всё это время тратилось на инициализацию TestBed.  Вырезание этой инициализации ускорило наши тесты в 10 раз. Интеграционные тесты могут быть быстрыми, но для этого нужно.. Более того, интеграционные тесты могут быть даже быстрее модульных, так как тестируемый код с реальными зависимостями лучше оптимизируется JIT-компилятором. А вот одноразовые моки практически не оптимизируются. Компонентные тесты могут вас подстраховать, когда вы где-то что-то недотестировали.  Тут тесты модуля DD выявили дефект, допущенный в модуле B. Его, конечно, придётся ещё поискать, но это всяко лучше, чем вообще о нём не узнать (как в случае модульных тестов) или узнать слишком поздно (в случае системных). Обратная сторона медали — дефект в одном модуле может заставить краснеть половину всех тестов и может быть сложно понять, где начинать искать дефект.  Тут мы приходим к идее фрактального тестирование, идея которого в том, чтобы отказаться от модульных и системных тестов, оставив только интеграционные. При этом писать их в форме компонентных тестов, образующих фрактальную структуру, где тесты более высокого уровня полагаются на то, что тесты уровня ниже уже всё проверили, а значит дефектов там скорее всего нет.  Если запускать компонентные тесты не в случайном порядке, как это обычно делают, а в порядке от менее зависимых к более зависимым, то первый же упавший тест укажет на тот модуль в котором скорее всего закрался дефект, а остальные тесты можно уже и не исполнять.  Например, мною написан MAM сборщик, который тесты и стили выстраивает в том же самом порядке, что и код в бандле. Это позволяет мне постоянно держать отладчик в режиме остановки на исключениях. И если какой либо тест упадёт, то отладчик тут же останавливается в нужном контексте, где тестируется сбойный модуль. Можно пойти ещё дальше, если прогон вообще всех тестов станет для вас обузой, и перезапускать лишь те тесты, что зависят от изменившегося кода.  Тут красным помечен изменившийся код. Зелёным — тесты, которые нет смысла перезапускать. А оранжевым — тесты, что требуется перезапустить. Причём опять же в порядке от менее зависимых к более зависимым. На каждом уровне мы имеем прямой доступ ко всем ручкам компонента, что недоступно, например, при системном тестировании. Причём тестировать нужно лишь добавляемую им функциональность, полагаясь на то, что уровень ниже уже протестирован и работает корректно.  Логика ровно такая же, что и со стандартной библиотекой или внешними зависимостями — мы их используем, но не мокаем и не тестируем, так как уверены в их работоспособности. Свой код тоже надо писать так, чтобы была такая уверенность. Просто используем компоненты как в реальном коде, безо всяких моков. Вот пример высокоуровневого компонентного теста.. Тут мы создаём экземпляр приложения, предоставив ему контекст окружения, который выдал нам тестовый фреймворк. В этом контексте уже замоканы все внешние и недетерминированные зависимости, что позволяет запускать приложение как бы в песочнице. Далее мы добавляем задачу через форму и проверяем, что она корректно добавилась, а форма после этого самоочистилась. Это предельно простой код, который сможет написать даже джун. И проверяет он ровно то поведение, что нам важно, не вдаваясь в детали рендеринга. Приведённый ранее тест пишется один раз и может быть запущен с разными уровнями изоляции путём подмены контекста окружения... И нам не нужно писать для этого разные типы тестов: от модульных до системных. У нас получилось довольно высокая степень тестового покрытия.. А поддержка, наоборот, сильно упростилась.. Сравнивая традиционную пирамиду с фрактальным подходом можно наблюдать следующую картину...  По пирамиде надо написать много разных тестов, а потом переписывать существенную долю модульных тестов при любом более-менее серьёзном рефакторинге. С фрактальным же тестированием кода и так нужно писать существенно меньше, так ещё и при рефакторинге достаточно лишь поправить тесты изменившихся контрактов и всё. Так что хватит уже вращать рожок тестирования. Его нужно просто выбросить. А писать лучше компонентные тесты во фрактальном стиле. Итого, мы довольно близко подошли к озвученному вначале идеалу.. Последний пункт тоже достижим при правильной архитектуре приложения. Приведу немного наблюдений их практики на примере моего рабочего проекта... Даже такого небольшого числа тестов хватает для обеспечения достойного уровня качества. Все тесты у меня прогоняются примерно за пол секунды, что позволяет запускать их прямо при старте приложения и получать максимально быструю обратную связь.  Василий Малыхин недавно рассказывал про эволюцию тестирования в их компании. Они сначала писали много модульных тестов, а потом начали считать и поняли, что профита от них мало, а затрат на свою поддержку требуют много.  Ну а Kent Beck, считающийся папой TDD, явно говорит, что старается мокать в своих "модульных" тестах как можно меньше, то есть фактически он пишет именно компонентные тесты.  Чтобы использовать фрактальное тестирование вам нужно разбить ваш монолит на компоненты, те на компоненты поменьше и так далее до неделимых компонент.  Если запуск компонентного теста у вас по времени сравним с запуском всего приложения, то возможно вам будет проще один раз его поднять и далее гонять на нём системные тесты. Только тесты для этого нужно будет писать особым образом в предположении, что состояние может быть изначально грязным. Пример такого теста я приводил ранее.  Даже в компонентных тестах приходится мокать такие вещи как рандом или внешний мир. Поэтому важно иметь механизм, позволяющий на верхнем уровне замокать любую низкоуровневую штуку. То есть нужна инверсия контроля. Есть разные паттерны его реализации: от синглтона, до контекста окружения.  Наконец, если у вас куча ресурсов для обеспечения качества, то вы можете позволить себе и модульное, и интеграционное, и системное, и мутационное тестирование. И даже тройное дублирование кода, написанного на разных языках разными людьми в разных парадигмах.  Full Stack Overflow