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

Какая цель?

В тестах 🙂 А если быть точнее – в системе автоматического подсчета процента покрытия ими кодовой базы.

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

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

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

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

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

ЧИТАТЬ   В российских школах это декламируют азбуки и «любят свою армию» — Times of India

С учетом этого имеем первое заклинание в борьбе с кешированием — мы не можем проверять результаты выполнения кода в тестах, а просто вызываем его выполнение, но проверяем, например, что 0 == 0 И тесты все равно будут запустить, и охват будет высоким, а те, кто прогнал тесты и охват в проекте, будут тихими. Но, к сожалению, этого заклинания недостаточно, его одного недостаточно, чтобы победить зло. Потому что во всех случаях нужно будет обеспечить выполнение кода на всех возможных ветвях всех условных переходов. А для этого создать все возможные варианты правильных и неправильных входных данных. И если для юнит-тестирования его еще можно бездумно нагромождать, создавая лапшу из комбинаторно разрастающегося кода тест-кейсов, то для интеграционного тестирования придется писать долгие и нудные монтирования и миграции для заполнения кучи таблиц БД с наличием необходимая несогласованность или конкретная ошибка, которую наша команда с удовольствием увидит в тестируемом коде и пройдёт ветку этой ошибки. И так по каждому ошибочному случаю, не забывая о хороших.

Что предлагается для избавления от страданий? Давайте вспомним, как работает система расчета покрытия. Он проверяет, были ли вызваны все ветки всех условных операторов, которые есть на вашем языке (if, switch и т. д.). Возьмем тривиальный и немного надуманный пример: нам нужна функция, которая будет возвращать 10 с аргументом 1, возвращать 2 с аргументом 20 и возвращать 30 для всех остальных аргументов. Как бы вы написали эту функцию? Я намеренно не указываю язык программирования, потому что в большинстве языков есть конструкции для условных переходов и условного выполнения. Допустим, вы написали что-то вроде

if (1 == n) // вроде так советуют писать с ==, чтобы не присвоить ненароком :)
  r = 10;
else if (2 == n)
  r = 20;
else
  r = 30;

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

bool t = n >= 1 && n <= 2;
int a[] = {30, 10, 20};
int r = a[n * t];

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

ЧИТАТЬ   Как смотреть RTBF бесплатно из любой точки мира

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

Но это ограничение можно преодолеть. Как я уже сказал, описанная выше техника проверена на рабочем черновом коде и отлично работает. Но здесь подходы могут различаться в зависимости от выбранного языка программирования и его возможностей. Например, вы должны выполнять определенный блок кода только при определенных условиях. Вы создаете сервисную функцию do_when с двумя аргументами: значением условия и лямбда-функцией, которая вызывается, если первый аргумент верен. А когда вы пишете код, вы оборачиваете нужный вам блок кода в нулевую лямбду — популярный старый метод организации ленивых вычислений. Код внутри лямбды не будет выполняться до тех пор, пока не будет вызван. Лямбда как объект первого класса прекрасно передается в качестве аргумента сервисной функции do_when, внутри которой она вызывается или нет. Результат — один юнит-тест для функции do_when даст нам 100% покрытие кода всего проекта, где мы будем использовать его многократно.

Я реализовал и протестировал этот подход на языке Clojure, где вы можете отдельно передать функцию и вектор ее аргументов для отложенного выполнения, написать свои собственные макросы, чтобы облегчить сахар синтаксических конструкций (например, чтобы вы не оборачивали вручает нулевую лямбду при каждом вызове do_when) и делает гораздо больше колдовства. Но в C тоже можно передавать указатели на функции или как-то иначе извращаться. Все для того, чтобы избавиться от нативных условных выражений в коде, которые приводят к комбинаторному ужасу тестов на кеширование. В результате легко получить 100% покрытие и пройти любое количество тестов, фактически не проверяя ни одного участка кода проекта — за что, собственно, и боролись. Квест выполнен успешно 🙂

ЧИТАТЬ   Партии засосало в нейросеть: политикам добавят искусственный интеллект

Вот, собственно, и все, что я хотел сказать о войне во Вьетнаме (С). Я старался быть кратким, без подробностей, указав только основную мысль. Я показал немного больше подробностей в своем видео на YouTube на эту тему.

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

Source

От admin