Как стать автором
Обновить

Фрактальное тестирование

Время на прочтение9 мин
Количество просмотров13K

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



Это — расшифровка выступления на TechLead Conf 2020. Вы можете посмотреть видео, прочитать как статью или открыть в интерфейсе проведения презентаций.


О себе


  • В программировании уже 25 лет
  • Последние 15 лет в основном фронт
  • Большие и маленькие проекты
  • Разработал фреймворк будущего

Путь в тестировании


  • Не писал тесты
  • Избегал тесты
  • Остался без тестировщиков
  • Пришлось разбираться самому
  • Набил кучу шишек
  • Познал дзен
  • Хочу поделиться с вами

Зачем тесты? Оперативная обратная связь!



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


Зачем тесты? Ускорение написания кода!


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

Чем раньше вы напишите автоматические тесты, тем больше вы сэкономите времени на ручном тестировании.


Зачем тесты? Локализация дефектов!



Хороший тест указывает на конкретное проблемное место.


Зачем тесты? Актуальная документация!


  • Отдельная документация всегда врёт
  • Даже если это комментарий рядом с кодом

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


Идеальные тесты: фиксируют лишь внешнее поведение


Проверяют всё важное, но не фиксируют неважное.



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


Идеальные тесты: баланс между числом и охватом



Их минимальное возможное число, чтобы охватить все необходимые тестовые сценарии.


Идеальные тесты: быстро и просто



Исполняются быстро, а писать просто.


Термины


  • Разные вещи часто называют одним именем
  • Одно и то же часто называют разными именами

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


Система (приложение)


Система — единица поставки, ваш продукт. Это то, что вы деплоите на сервер, запускаете на машине пользователя и тп.



Тест системы называется "системным тестом" или "E2E тестом".


Модуль (юнит)


Модуль — единица кода. Как правило это файл или даже часть этого файла.



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


Компонент (подсистема)


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



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


Рожок тестирования


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



Это мало того, что медленно. Так ещё и из-за человеческого фактора пропускает много дефектов.


Пирамида тестирования


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



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


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



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


Модульные тесты: Ломают абстракции


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


sum( 1 , 2 )

function sum( a , b ) {
    logger.trace( a , b )
    return algebra.apply( '+' , a , b )
}

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


new Sum( algebra , logger ).exec( 1 , 2 )

class Sum {

    constructor(
        private algebra,
        private logger,
    ) {}

    exec( a , b ) {
        logger.trace( a , b )
        return algebra.apply( '+' , a , b )
    }

}

Модульные тесты оказывают довольно сильное (и далеко не всегда положительное) влияние на архитектуру и собственно код.


Модульные тесты: Хрупкие



Вы изменили интерфейс модуля B. Теперь вам надо не только изменить условно 10 его тестов, но и 10 моков в модульных тестах зависимых модулей. И соответственно по 10 тестов самих этих модулей, хотя их контракт ничуть не изменился.


Критерии бесполезности теста


Поэтому можно сформулировать критерий бесполезности теста. Если:


  • Желаемое поведение не изменилось
  • Тест упал

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


  • Желаемое поведение изменилось
  • Тест не упал

То этот тест не проверяет то, что должен, что ещё хуже.


Модульные тесты: Не полны


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



Системные тесты: Не масштабируются


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



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


Системные тесты: Хрупкие



Падение какой-то мелочи с краю может запросто обрушить вам половину тестов. И иди ищи потом кто же виноват.


Интеграционные тесты: Медленно


Они поднимают несколько модулей. И выполняют реальную работу, что может быть медленно. Кроме того, беда может быть в вашем тестовом фреймворке. Например, однажды мы обратили внимание, что любой, даже самый тривиальный тест компонента в Ангуляре занимает 100мс. Стали разбираться и выяснили, что всё это время тратилось на инициализацию TestBed.



Вырезание этой инициализации ускорило наши тесты в 10 раз.


Интеграционные тесты: Быстро


Интеграционные тесты могут быть быстрыми, но для этого нужно..


  1. "Ленивая" архитектура приложения
  2. Быстрый старт тестов

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


Компонентные тесты: Подстраховка


Компонентные тесты могут вас подстраховать, когда вы где-то что-то недотестировали.



Тут тесты модуля DD выявили дефект, допущенный в модуле B. Его, конечно, придётся ещё поискать, но это всяко лучше, чем вообще о нём не узнать (как в случае модульных тестов) или узнать слишком поздно (в случае системных).


Компонентные тесты: Где ошибка?


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



Фрактальное тестирование


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



Фрактальное тестирование: Вот ошибка!


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



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


Фрактальное тестирование: Кратчайший путь


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



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


Фрактальное тестирование: Постоянная сложность


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



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


Фрактальное тестирование: Простота написания


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


const app = new Todomvc({ context })

const title = guid()
const rowsPrev = app.rows()

app.NewTitle().value( title )
app.NewSubmit().click()

assertEqual( app.rows()[0].title() , title )
assertEqual( app.rows().slice(1) , rowsPrev )
assertEqual( app.NewTitle().value() , '' )

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


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


Фрактальное тестирование: Уровни изоляции


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


  1. Без браузера под NodeJS
  2. Без сервера в разных браузерах
  3. С тестовым сервером
  4. Вообще на проде

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


Фрактальное тестирование: Качество


У нас получилось довольно высокая степень тестового покрытия..


  • Тестируются все модули
  • Тестируются все взаимосвязи
  • На каждом уровне есть все необходимые ручки
  • В том числе и вся система в сборе
  • В том числе в разных окружениях

Фрактальное тестирование: Простая поддержка


А поддержка, наоборот, сильно упростилась..


  • По количеству нужно меньше, чем модульных
  • Код проще, чем у модульных
  • Скорость исполнения сравнима с модульными
  • Падают лишь при изменении значимого поведения

Сравнение трудоёмкости


Сравнивая традиционную пирамиду с фрактальным подходом можно наблюдать следующую картину...



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


Так что хватит уже вращать рожок тестирования. Его нужно просто выбросить. А писать лучше компонентные тесты во фрактальном стиле.


Близость к идеалу


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


Критерий Достигнут?
Фиксируют лишь внешнее поведение +
Баланс между числом и охватом +
Писать быстро и просто +*

Последний пункт тоже достижим при правильной архитектуре приложения.


Мой проект


Приведу немного наблюдений их практики на примере моего рабочего проекта...


  • Пилю соло второй год
  • Весьма сложный web-виджет
  • Конкурентов меньше 5
  • Рефакторинг каждый месяц
  • 300 компонентных тестов

Даже такого небольшого числа тестов хватает для обеспечения достойного уровня качества.


Прогон тестов при запуске приложения


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



Опыт других людей


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



Ну а Kent Beck, считающийся папой TDD, явно говорит, что старается мокать в своих "модульных" тестах как можно меньше, то есть фактически он пишет именно компонентные тесты.


Ограничения: Монолитная архитектура



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


Ограничения: Тяжёлая архитектура



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


Ограничения: Нет инверсии контроля



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


Ограничения: Высокая стоимость ошибки



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


Хочется большего?



Отзывы


  • 1 — Ничего нового.
  • 2 — Почему-то показалось, что автор рекламирует себя, а не рассказывает о полезном другим. Также крутость заявки завысила ожидания. А «тушить» было нечего...
  • 3 — Я ценю то, что докладчик выступил со своей наработанной идеей, но, по моему мнению, это не применимо.
  • 3 — Простой материал, долгое введение.
  • 3 — Не понял, зачем новое название?
  • 4 — Интересный подход, достоин внимания. Я даже сделал себе запись этого доклада и пересматривал 2 раза.
  • 4 — Идея понравилась, но саморекламы чуть-чуть перебор.
  • 5 — Противоречивый докладчик, но доклад довольно интересный, стоит глубже вникнуть с тему.
  • 5 — Доклад был интересен и применим.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какая у вас конфигурация тестов?
7.61% Рожок тестирования7
14.13% Рюмка тестирования13
8.7% Пирамида тестирования8
3.26% Фрактал тестирования3
54.35% А что такое тесты?50
11.96% Что-то ещё11
Проголосовали 92 пользователя. Воздержались 37 пользователей.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+9
Комментарии26

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн