Когда я пишу тест, я часто не уверен, что мой дизайн будет на 100% успешным.
И я хочу, чтобы это давало гибкость при рефакторинге кода — например, чтобы потом менять класс, не меняя тестовый код.
Но если у меня будет стандартная пирамида, внизу которой будет много юнит-тестов, то не получится ли, что тесты будут знать не о поведении системы, а о том, какие классы там есть?
Всем привет! Это стенограмма подкаста «Между скобками» — моих интервью с интересными людьми из мира PHP-разработки.
Запишите, если хотите послушать.
В полной аудиоверсии мы также обсудим дополнительные вопросы покрытия кода.
С Владимиром вянц Мы встретились с Янцем в Февральский PHP-митап в Ростове : Я рассказал о своем опыте работы с асинхронностью, он дал отчет о тестах.
У меня остались вопросы из того выступления, и в период карантина мы созванивались, чтобы их обсудить.
Сергей Жук, Skyeng: Начнем с главного.
Нужны ли модульные тесты? Рискую ли я получить слишком большую связность между тестом и кодом? И проект наверняка изменится, почему бы мне не пойти с вершины пирамиды тестирования, с теми же функциональными тестами? Владимир Янц, Badoo: Это очень хороший вопрос.
Начнем с того, нужны ли они в принципе.
Может, действительно только функциональные? Функциональные тесты — это круто, но они охватывают немного другие потребности.
Они позволяют вам убедиться, что все приложение или часть функции работает. И у них своя проблема - стоимость поддержки, написания и запуска.
Например, в настоящее время у нас есть 20 000 функциональных приемочных тестов.
Если вы запустите их в одном потоке, они будут работать несколько дней.
Соответственно, нам нужно было придумать, как за считанные минуты получить результат: отдельные кластеры, куча машин, большая массивная инфраструктура для поддержки всего этого.
А еще ваш тест зависит от окружения: другие серверы, что есть в базе, кеш.
И чтобы это исправить, нужно потратить много ресурсов, денег и времени разработчиков.
Что хорошего в модульном тесте? Вы можете протестировать всевозможные сумасшедшие случаи.
Функционально вы не можете проверить, как функция отреагирует на отрицательное значение.
Вы можете сразу проверить все случаи, чтобы убедиться в идеальной работе функции: поставщики данных создают довольно много вариантов, а запуск опции занимает доли секунды.
Поэтому отвечая на главный вопрос.
Нужны ли хорошие модульные тесты? Да.
Нужны ли плохие? Нет. Один из основных принципов тестирования заключается в том, что тест можно вкладывать в контракт, а не в реализацию.
Контракт — это соглашение о том, что ваш метод или функция принимает в качестве входных данных и что они должны с этим делать.
Действительно, проекты на начальных стадиях могут менять свою архитектуру; хрупкие тесты здесь будут бесполезны.
Но я бы начал писать модульные тесты как можно раньше: не рассматривая хрупкие и зависящие от архитектуры части.
Ведь есть места, где сразу понятно, что рефакторить их не будешь.
Допустим, вам нужно посчитать сложные вещи, у вас есть помощник, который вы используете много раз.
Вряд ли вы измените логику этих расчетов.
А пройдя его юнит-тестом, вы будете уверены, что все делается правильно.
Сергей Жук, Skyeng: Ок, смотрите, у меня есть какое-то веб-приложение, и я начинаю в нем писать юнит-тесты.
Допустим, я знаю, что у этого класса может быть 5 разных входных данных, я меняю реализацию и просто делаю модульный тест для провайдера, чтобы не тестировать каждый раз.
А еще есть какая-то опенсорсная либа — тут без юнит-теста тоже не обойтись.
А для каких еще случаев они необходимы и не стоит писать? Владимир Янц, Badoo: Я бы писал тесты там, где есть какая-то бизнес-логика: не в базе данных, а в PHP-коде.
Некоторые помощники, которые что-то вычисляют и выводят, некоторые бизнес-правила — отличные кандидаты для тестирования.
Еще я видел, что в приложении пытались тестировать тонкие контроллеры.
Главное, что должны сделать модульные тесты, — это спасти чистую функцию.
Независимо от того, сколько раз вы вызываете чистую функцию с входными значениями, она всегда будет давать один и тот же результат. Это детерминированный подход. Если существует публичный метод класса, который имеет критерии чистой функции и его легко сломать, это очень хорошая история для теста.
Наоборот, если у вас какая-то админка с CRUD и большим количеством одинаковых форм, юнит-тесты особо не помогут. Логики мало; тестирование такого кода изолированно от базы данных и среды проблематично.
Даже с архитектурной точки зрения.
Здесь лучше сдать тесты более высокого уровня.
Сергей Жук, Skyeng: Когда я начинал, мне казалось, что круто сделать модульное тестирование максимально подробным.
Вот у меня есть объект, у него есть какой-то метод и несколько зависимостей (например, модель, которая поступает в базу данных).
Я промочил все.
Но со временем я понял, что ценность таких тестов равна нулю.
Они проверяют код на наличие опечаток, а также встраивают в него тесты.
Но я до сих пор общаюсь с теми, кто за такой «подход к работе».
Какова ваша позиция: нужно ли так активно мочиться? И в каких случаях оно действительно того стоит? Владимир Янц, Badoo: В общем, мокнуть полезно.
Но одна из самых вредных структур, которая, как мне кажется, есть в PHPUnit — это ожидания.
Например, плохо, когда люди пытаются сделать так, чтобы метод вызывался с определённым набором параметров и определённое количество раз.
Мы не пытаемся протестировать контракт. Хороший тест этого не делает.
Это делает тесты очень хрупкими.Вы потратите время на его исправление, а вероятность поймать баг очень мала.
В один момент вам станет скучно.
Я видел такое: ребята начали писать модульные тесты, сделали это неправильно и в какой-то момент забросили инструмент, потому что он был бесполезен.
Давайте возьмем функцию rand().
В качестве входных данных он принимает два числа от 0 и выше и должен возвращать случайное число между этими двумя.
Это контракт. В этом случае можно проверить, работает ли он на граничных значениях и так далее.
Если у вас есть какой-то помощник, который складывает два числа в модели, вам следует создать поддельный объект, чтобы проверить, что помощник правильно складывает числа.
Но нет необходимости проверять, сколько раз он дёрнул эту модель, зашёл ли в свойства или дёрнул геттер — это детали реализации, которые не должны быть предметом тестирования.
Сергей Жук, Skyeng: Итак, вы начали о вреде ожиданий.
Моки о них.
Нужны ли вообще моки, если есть фейки и заглушки? Владимир Янц, Badoo: Проблема заглушек в том, что они не всегда удобны: их необходимо заранее создать и описать.
Это удобно, когда у вас есть объект, который часто используется повторно — вы написали его один раз, а потом используете во всех тестах, где это необходимо.
А для объекта, который вы используете в одном-двух тестах, написание заглушки занимает очень много времени.
Поэтому я активно использую моки в качестве заглушек.
Вы можете создать макет, заблокировать все необходимые функции и работать с ними.
Я использую их только таким образом.
Ну, фейки — мой любимый подход. Это очень помогает протестировать много вещей.
Например, когда вы хотите проверить, правильно ли вы все кэшировали.
Для Memcached легко создать поддельный объект; он включен в большинство стандартных платформ.
Для ввода вы используете интерфейс, а не конечный класс, и ваш подделку можно безопасно передать в качестве аргумента любой функции.
А внутри, вместо обращения к самому кешу, вы реализуете функционал кеша внутри объекта, создавая массив.
Это значительно упрощает тестирование, поскольку вы можете проверить логику с помощью кеширования — иногда это важно.
Сергей Жук, Skyeng: Посмотрите, еще одна крайность.
Я встречал людей, которые говорили: «Хорошо, как мне протестировать частный метод/классЭ» Владимир Янц, Badoo: Бывает, что код структурирован не очень правильно, и правильный метод может быть отдельной чистой функцией, реализующей функционал.
По-хорошему, это надо было бы вынести в отдельный класс, но оно почему-то в защищенном элементе.
В крайнем случае, вы можете проверить это с помощью модульного теста.
Но лучше сделать это отдельной сущностью, у которой будет свой тест. Что-то вроде собственного явного контракта.
Сергей Жук, Skyeng: Вот вы говорите, есть договор, а остальное — детали реализации.
Если мы говорим о веб-приложении с точки зрения интерфейса: у него есть контракт, по которому оно общается с пользователями.
При этом мы создаем запрос, отправляем его, проверяем ответ, побочные эффекты, базу данных и что-то еще.
Если вы делаете модульные тесты, вы можете вынести логику связи с базой данных в отдельный слой, создать интерфейс для репозитория и создать отдельную реализацию репозитория в памяти для тестов.
Но стоит ли оно того? Владимир Янц, Badoo: У вас есть приложение, которое должно работать корректно в целом.
А модульные тесты должны охватывать некоторые важные вещи, ответы на которые сильно варьируются и которые можно легко изолировать в чистую функцию.
Вот где они должны быть в первую очередь.
Юнит-тесты могут дать самую быструю обратную связь: запустите их и получите результаты через минуту.
Если обнаружена ошибка, маловероятно, что все приложение будет работать правильно, если только с какой-то его частью что-то не так.
Не зря умные люди придумали пирамиду тестирования.
Модульные тесты выполняются быстро, и их можно написать много.
В этом их цель — дешево протестировать многие части вашего приложения, чтобы убедиться, что они работают правильно.
Чем больше частей покрыто юнит-тестами, тем лучше, но это не значит, что нужно стремиться к 100% покрытию.
Есть много вещей, которые тесты не уловят, потому что они не предназначены для этого.
Но если что-то получится на их уровне, нет смысла идти дальше и проводить тяжелые функциональные тесты.
На этапе функциональных или интеграционных тестов не нужно тестировать вариативность ответа.
Вы проверили верхний уровень, все компоненты в совокупности работают так, как вы ожидаете.
Теперь вам не нужно создавать огромное количество провайдеров и интеграционных тестов для каждого случая.
В этом смысл пирамиды.
Сергей Жук, Skyeng: Давайте, наконец, поговорим о мутационных тестах.
Они необходимы? Владимир Янц, Badoo: Вам необходимо реализовать мутационные тесты, как только вы подумаете о модульных тестах.
Они поднимут из-под ковра все те проблемы, о которых так часто говорят - бесполезное тестирование, покрытие ради покрытия.
Ничего дополнительно писать там не нужно.
Это просто библиотека: она берет покрытие, которое у вас где-то есть, заходит в исходный код и начинает менять операторы кода на противоположные.
После каждой такой мутации он запускает тесты, которые по покрытию кода перекрывают эту строку.
Если ваши тесты не проваливаются, они бесполезны.
И, наоборот, можно найти линии, у которых есть потенциальные мутации, но нет покрытия.
Это ничего не стоит, чтобы реализовать это на ранней стадии.
И есть много преимуществ.
Теги: #разработка сайтов #тестирование #php #подкасты #дизайн и рефакторинг #мутационное тестирование #отладка #функциональное тестирование #моки #юнит-тесты #между скобками #заглушки
-
Внешнее Хранилище Данных Работает На Вас
19 Oct, 24 -
Как Warcraft 3 Помог Мне Выучить Пару Языков
19 Oct, 24 -
Конкурентное Развитие
19 Oct, 24