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

Тест-ревью: как прошли два года написания unit-тестов

Время на прочтение10 мин
Количество просмотров7.1K
Всего голосов 17: ↑13 и ↓4+10
Комментарии23

Комментарии 23

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

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

Расскажите про константу подробнее, в чем смысл её? Звучит как костыль

Наверное велосипед для https://github.com/auchenberg/volkswagen (Volkswagen detects when your tests are being run in a CI server, and makes them pass.)

Ну что поделать, для Вас звучит, для меня нет)

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

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

Естественно тестмод отключается когда запускаются "главные тесты" тестирующие всю систему в комплексе.

Не по теме: в корзине, у вас в приложении, нельзя поменять размер пиццы, только удалить и выбрать заново. Не очень удобно, если подбираешь заказ на какую-то сумму, например. Request for change)

Согласен! Задачка в беклоге, но сроков нет :-)

Попробуйте этот подход. Он позволяет существенно уменьшить число и сложность тестов.

Спасибо за ссылку! Про подобное в следующий раз расскажу.

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

Что-то в этом роде:

Create another HTML file that shows the submit history to users.

Each data submission should be represented as a div block with the submit-history-card class. Each div block should contain the following elements:

  1. <p> element with the card-first-name class. Inside this tag, show the first name from the submitted form;

  2. <p> element with the card-last-name class. Inside this tag, show the last name from the submitted form;

  3. <p> element with the card-email class. Inside this tag, show the email from the submitted form.

  4. <p> element with the card-phone class. Inside this tag, show the phone from the submitted form.

  5. <p> element with the card-company class. Inside this tag, show the company from the submitted form.

  6. <p> element with the card-address class. Inside this tag, show the address from the submitted form.

  7. <button> element with the delete-button class. Clicking on it should do nothing for now.

Both pages should contain the navigation bar with the following elements:

  1. <a> tag with the form-link ID. When users click on this link, navigate them to the main page with the form. To do so, you should set the href attribute value to the path of the main HTML file;

  2. <a> tag with the history-link ID. When this link is clicked you should navigate to the history page. To do so you should set the href attribute value to the path to the history HTML file.

Once users visit the history page, get all the history of the submission and create a div block with the submit-history-card class for each submission. Add it to the DOM.

After submitting the main page, clear all the input fields on the form.

Проверяется оно платформой через своего рода интеграционный тест. Т.е. оно выдает ошибки типа: "кнопка с идентификатором submit-button не найдена!"

Так вот у меня все время крутится в голове мысль, почему мы не пишем приложения в таком режиме? Когда Т.З. раскладывается архитекторами на технические составляющие, которые можно проверять автоматически.

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

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

Есть компании работающие на смеси всяких DD (BDD/ADD/TDD). Проблема в том, что это сложно организационно. Мне довелось так поработать год и менеджмент кипел и плавился вместе со всеми консультантами по теме. Вы переложили все функции на архитектора, но вот он (я) оказался не согласен. Во первых архитектор один-два на 5-6 команд. Как вы представляете себе разработку, когда все жду тестов на 60 человек в следующий спринт и на это выделили 2 человека? И давайте откровенно, не человека, а 2 оч. дорогих ресурса. Во вторых, чтоб даже просто обозначить тесты (допустим только имя и ассерты), необходимо, чтоб стори был разжёван, и в голове архитектора уже был прототип кода. Получается надо детально проработать весь бэклог спринта всех команд. И тут оказывается, что такой детализации никто не хочет. Приходит стори в котором есть 2-3 сценария (успешный и пара ошибок), я вижу, что там возможно еще 4-5 ошибок. Надо обговорить их с продактом. Начинается пинг-понг, в котором продакт не заинтересован (за тесты же отвечает архитектор), да и сейчас надо срочно в прод, а потом уже как-нибудь посмотрим. Мы уже не говорим об а/б и продуктовых гипотезах, про которые никакой консультант не думал, когда жестко привязал ALM шаги. А разработчик будет возмущаться и говорить, что ему не интересно тупо писать код в тех строгих рамках, которые ему выстроили. Он не джун на экзамене, а опытный творец.

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

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

Все верно говорите. Но есть нюанс. Если Вам приходится рефакторить тесты, то Вы пишите неправильные тесты :)

Попробуйте подумать, что же не так с Вашими тестами.

Несколько подсказок.

  1. Тесты – это ТЗ, переведенное в код. Если ТЗ не менялось, тест тоже меняться не должен. Если Вы вынуждены менять тест при рефакторинге кода, то Ваш тест связан с кодом. Это ошибка. Связность в коде стараются избегать, а про то, что тест тоже не должен зависеть от кода, обычно забывают (от публичных интерфейсов тесты могут и должны зависеть, от деталей реализации – не должны)

  2. Вторая самая распространенная ошибка – это тесты, проверяющие все. Этакие God-тесты, когда в одном тесте проверяется сразу 100500 условий и поведений. В идеале тут тоже должен выполняться принцип единой ответственности – каждый тест должен падать строго от одной и только одной ошибки. Если у Вас при ошибке падает несколько тестов, Вы снова пишите их неверно. Снова связность. Тут, кажется, очень хорошо помогают мутационные тесты: вы сразу видите сколько тестов у вас сломалось от каждой мутации, и вынуждены думать, как же так их поменять, чтобы они конкретную мутацию проверили, но не зависели от остального кода.

  3. Принципы FIRST. Много полезного. Особенно буковка I - которая снова про изоляцию и связность.

Итог. Тесты не должны обновляться и рефакториться, если не менялось ТЗ.

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

А эта статья была? Что-то не нашел в Ваших статьях. С радостью бы почитал.

Рефакторинг это изменение кода без изменения поведения. Поэтому тесты отлично рефакторятся :-)

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

Нет, Вы, кажется, не поняли. Рефакторите Вы код, но не тесты. Вы верно пишите, что поведение не меняется. Значит, не должны меняться и тесты. Значит, рефакториться они не должны :) Ну, смысла в этом тупо нет. Они есть, они проверяют поведение. Точка. Зачем их рефакторить? Ну, это понятно для кода: сделать его быстрее (в 99% случаев тоже сомнительно), сделать его читабельнее, масштабируемее, переиспользуемее... А тесты зачем рефакторить?

За видео спасибо. А не подскажете, таймфреймы по теме для экономии времени?

Сделаю шаг назад: тесты это код который надо поддерживать. Примеров много:

  • в свифте появился async/await, код переходит на него. Функционально ничего не меняется, но синтаксис вызова другой, приходиться менять.

  • Добавили фичу и синтаксис старой поменялся, чтобы лучше описывать абстракцию.

  • Мигрировали одну сетевую либу на другую: обновляем места интеграции.

  • Решили отказаться от спецификаций из-за скорости компиляции? Переписываем на XCTest.

  • Ну и т.д

Да, тесты можно написать так, чтобы это все было абстрагировано. Написаны ли они так фактически? А те тесты, которые 3 года назад написали? Скорее нет

В первых трех случаях вы нарушили Open/Close принцип в вашем коде. Вы изменили поведение, а должны были расширить его. Например, достаточно было завести вторую функцию, которая бы вызывала первую, но оборачивала бы ее в async/await. В таком случае Вам не нужно переписывать ни одного теста (и ни одной строчки кода!), достаточно написать пару новых тестов на новую функцию, которые будут очень примитивны: один проверит вызов функции, второй – возврат колбека. Тот самый SOLID, от которого все плюются, на практике :)

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

Могу ли я по приведенной Вами выборке сделать в первом приближении вывод, что более 75% работы Вы делаете зря? При этом из-за Ваших же ошибок в коде?

И да, я тоже откачусь назад и слегка подправлю свой изначальный тезис: "Если Вам приходится рефакторить тесты, то Вы пишите неправильные тесты И неправильный код" :)

В общем, завязываю мешать Вам рефакторить тесты, пойду смотреть видюху! Спасибо еще раз Вам за нее.

Я понял, у вас неприязнь к OCP и SOLID. Ок, специально для Вас: замените OCP в моей предыдущей реплике на следующую цитату из приведенной видюхи, смысл останется неизменным и справедливым:

"Не ломай публичный контракт без необходимости!"

Ну, или скажу другими словами. Вместо рефакторинга тестов в первых трех пунктах у@akaDualityнадо было просто не ломать публичные интерфейсы.

Кстати, я сам OCP понимаю исключительно, как постоянство публичных интерфейсов и отсутствие в них ломающих изменений в терминологии symver.

Желаете что-то возразить или поправить меня?

А за ссылку спасибо, полистаю, что там у Вас еще имеется.

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

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

Вот прям необходимо?

Если старая функция больше не нужна, то да, необходимо.

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

Это всё не является ценностью само по себе. А вот различные ресурсы она потребляет даже если не используется.

Я полистал доклад, до 11 минуты вроде база, а потом про модули, зависимости и тесты все. Норм на двойной скорости смотрится

Зарегистрируйтесь на Хабре, чтобы оставить комментарий