1 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
<p><strong>Как начать писать тесты? Сколько нужно писать? На что их нужно писать, а на что - не нужно? Стоит ли всегда применять TDD? Если вас интересуют ответы на эти вопросы, то вы читаете правильную статью. В своей жизни я написал не одну тысячу тестов всех мастей для разных платформ, использовал во всех полях TDD и ставил процесс тестирования в командах, проектах и даже целых компаниях. И теперь я попробую обобщить этот опыт и поделиться им.</strong></p>
1
<p><strong>Как начать писать тесты? Сколько нужно писать? На что их нужно писать, а на что - не нужно? Стоит ли всегда применять TDD? Если вас интересуют ответы на эти вопросы, то вы читаете правильную статью. В своей жизни я написал не одну тысячу тестов всех мастей для разных платформ, использовал во всех полях TDD и ставил процесс тестирования в командах, проектах и даже целых компаниях. И теперь я попробую обобщить этот опыт и поделиться им.</strong></p>
2
<p>Тестирование, как и многое в программировании, стало культом карго. Вместо осознанного движения, разработчики пытаются следовать популярным методологиям, слепо верить тому, что пишут в документации, покрывать код на 100% тестами. Я был свидетелем удаления папки с тестами (в 40 тысяч строк кода) по причине того, что их стало невозможно поддерживать. Такое тестирование чаще приводит к обратному эффекту - разработка становится дороже, а процесс медленнее, и даже если наблюдается позитивный эффект, то он дается слишком дорого.</p>
2
<p>Тестирование, как и многое в программировании, стало культом карго. Вместо осознанного движения, разработчики пытаются следовать популярным методологиям, слепо верить тому, что пишут в документации, покрывать код на 100% тестами. Я был свидетелем удаления папки с тестами (в 40 тысяч строк кода) по причине того, что их стало невозможно поддерживать. Такое тестирование чаще приводит к обратному эффекту - разработка становится дороже, а процесс медленнее, и даже если наблюдается позитивный эффект, то он дается слишком дорого.</p>
3
<p>Основная цель этой статьи - дать вам целостное понимание смысла тестирования. Понимая суть, вы сможете лучше мыслить критически и понимать, к чему нужно идти. Ну и, конечно, будет немного практических советов.</p>
3
<p>Основная цель этой статьи - дать вам целостное понимание смысла тестирования. Понимая суть, вы сможете лучше мыслить критически и понимать, к чему нужно идти. Ну и, конечно, будет немного практических советов.</p>
4
<blockquote><p>Подписывайтесь на<a>канал Кирилла Мокевнина в Telegram</a>- чтобы узнать больше о программировании и профессиональном пути разработчика</p>
4
<blockquote><p>Подписывайтесь на<a>канал Кирилла Мокевнина в Telegram</a>- чтобы узнать больше о программировании и профессиональном пути разработчика</p>
5
</blockquote><p><strong>Начнем, пожалуй, с самого главного вопроса: зачем нам вообще нужно тестировать?</strong></p>
5
</blockquote><p><strong>Начнем, пожалуй, с самого главного вопроса: зачем нам вообще нужно тестировать?</strong></p>
6
<p>Чтобы быть уверенными в работоспособности нашего продукта. Заметьте, что я не написал "функций", "модуля", "кода" или "проекта". В конечном итоге имеет значение только то, что конечный продукт, которым пользуются (не всегда пользователи), работает, и делает он это хорошо. Хотя прямо сейчас это может показаться капитанством, но, как вы увидите позже, ориентация на цель позволит нам принимать правильные решения.</p>
6
<p>Чтобы быть уверенными в работоспособности нашего продукта. Заметьте, что я не написал "функций", "модуля", "кода" или "проекта". В конечном итоге имеет значение только то, что конечный продукт, которым пользуются (не всегда пользователи), работает, и делает он это хорошо. Хотя прямо сейчас это может показаться капитанством, но, как вы увидите позже, ориентация на цель позволит нам принимать правильные решения.</p>
7
<p>Следующий ключевой тезис не является особенностью процесса тестирования. Задачи можно условно поделить на два типа: они либо завершены, либо нет, а завершенность задач второго типа - это шкала, где 0 - это "ничего не сделано", а 1 - это сделано на 100%. При решении таких задач 100%-решение часто оказывается недостижимым из-за сверхвысоких накладных расходов.</p>
7
<p>Следующий ключевой тезис не является особенностью процесса тестирования. Задачи можно условно поделить на два типа: они либо завершены, либо нет, а завершенность задач второго типа - это шкала, где 0 - это "ничего не сделано", а 1 - это сделано на 100%. При решении таких задач 100%-решение часто оказывается недостижимым из-за сверхвысоких накладных расходов.</p>
8
<p>Приведу прекрасный пример. Для многих сервисов критично такое понятие как SLA или, проще говоря, доступность сервиса. Например, на хостинговых площадках пишут что-то в духе "мы обеспечиваем доступность 99.9% наших серверов". Давайте прикинем, сколько часов за год хостинг может оказаться недоступен в рамках его SLA: 0.001 * 365 * 24 = 8.7. В принципе, неплохо.</p>
8
<p>Приведу прекрасный пример. Для многих сервисов критично такое понятие как SLA или, проще говоря, доступность сервиса. Например, на хостинговых площадках пишут что-то в духе "мы обеспечиваем доступность 99.9% наших серверов". Давайте прикинем, сколько часов за год хостинг может оказаться недоступен в рамках его SLA: 0.001 * 365 * 24 = 8.7. В принципе, неплохо.</p>
9
<blockquote><h3>Читайте также:</h3>
9
<blockquote><h3>Читайте также:</h3>
10
<p>Чем<a>отличается обучение</a>на Хекслете от других школ</p>
10
<p>Чем<a>отличается обучение</a>на Хекслете от других школ</p>
11
</blockquote><p>Предположим, что обеспечение такого уровня доступности обходится компании в 1000$. А во сколько обойдется компании добавление каждой новой девятки в конце? То есть обеспечение 99.99, 99.999 и так далее. Насколько мне известно, на таком уровне обеспечения происходит экспоненциальный (взрывной) рост стоимости. Я уже не говорю про то, что 100%-доступность является фантастикой.</p>
11
</blockquote><p>Предположим, что обеспечение такого уровня доступности обходится компании в 1000$. А во сколько обойдется компании добавление каждой новой девятки в конце? То есть обеспечение 99.99, 99.999 и так далее. Насколько мне известно, на таком уровне обеспечения происходит экспоненциальный (взрывной) рост стоимости. Я уже не говорю про то, что 100%-доступность является фантастикой.</p>
12
<p>Этот пример ярко демонстрирует то, что в задачах с плавающим результатом главным принципом является "максимальный результат за минимальные ресурсы". Другими словами, ищется баланс, при котором мы получаем результат, удовлетворяющий стейкхолдеров (заинтересованных лиц), за приемлемый бюджет/сроки.</p>
12
<p>Этот пример ярко демонстрирует то, что в задачах с плавающим результатом главным принципом является "максимальный результат за минимальные ресурсы". Другими словами, ищется баланс, при котором мы получаем результат, удовлетворяющий стейкхолдеров (заинтересованных лиц), за приемлемый бюджет/сроки.</p>
13
<p>Теперь возвращаемся к нашим тестам и обнаруживаем, что тесты относятся именно к этому типу задач. Добавление первых тестов в проект дает невероятный эффект. Покрытие в 50% (половина кода вызывается в тестах) получается почти сразу, и по сравнению с отсутствием тестов - мы на два корпуса впереди. Дальше ситуация начинает меняться, и где-то на уровне 70-90% начинается резкое замедление роста покрытия, тесты становятся все более точечными, дорогими. Возрастает сложность их поддержки, рефакторинга.</p>
13
<p>Теперь возвращаемся к нашим тестам и обнаруживаем, что тесты относятся именно к этому типу задач. Добавление первых тестов в проект дает невероятный эффект. Покрытие в 50% (половина кода вызывается в тестах) получается почти сразу, и по сравнению с отсутствием тестов - мы на два корпуса впереди. Дальше ситуация начинает меняться, и где-то на уровне 70-90% начинается резкое замедление роста покрытия, тесты становятся все более точечными, дорогими. Возрастает сложность их поддержки, рефакторинга.</p>
14
<p>Этот процесс бесконечен. Добиться 100% покрытия<strong>очень</strong>дорого и, скорее всего, неоправданно (см. пример выше). Кроме того, никакие тесты не дают вам полную гарантию работоспособности.</p>
14
<p>Этот процесс бесконечен. Добиться 100% покрытия<strong>очень</strong>дорого и, скорее всего, неоправданно (см. пример выше). Кроме того, никакие тесты не дают вам полную гарантию работоспособности.</p>
15
<p>Кроме количества тестов и их качества, на стоимость также влияет то, какой тип тестов мы используем. Существует множество классификаций видов тестов, таких как: "по знанию системы", "по степени автоматизации", "по времени проведения тестирования". На этом этапе нас интересует только одна классификация: "по степени изолированности компонентов":</p>
15
<p>Кроме количества тестов и их качества, на стоимость также влияет то, какой тип тестов мы используем. Существует множество классификаций видов тестов, таких как: "по знанию системы", "по степени автоматизации", "по времени проведения тестирования". На этом этапе нас интересует только одна классификация: "по степени изолированности компонентов":</p>
16
<ul><li>Модульное тестирование</li>
16
<ul><li>Модульное тестирование</li>
17
<li>Интеграционное тестирование</li>
17
<li>Интеграционное тестирование</li>
18
<li>Системное тестирование (приемочное)</li>
18
<li>Системное тестирование (приемочное)</li>
19
</ul><p>Именно здесь начинаются проблемы. Во-первых, идут настоящие религиозные войны на тему того, что называть модульным тестированием, а что не называть. Во-вторых, эти типы тестов подаются как нечто конкретное с большим количеством требований для того, чтобы соответствовать одной из этих категорий. Такое положение вещей приводит к тому, что программисты думают, в первую очередь, не о результате, а о том, пишут ли они юнит-тесты в соответствии с канонами, или нет.</p>
19
</ul><p>Именно здесь начинаются проблемы. Во-первых, идут настоящие религиозные войны на тему того, что называть модульным тестированием, а что не называть. Во-вторых, эти типы тестов подаются как нечто конкретное с большим количеством требований для того, чтобы соответствовать одной из этих категорий. Такое положение вещей приводит к тому, что программисты думают, в первую очередь, не о результате, а о том, пишут ли они юнит-тесты в соответствии с канонами, или нет.</p>
20
<p>В действительности же нет никаких четких разделений на три уровня. Даже если вы тестируете чистую функцию (модульное тестирование), она выполняется на конкретном железе, и потенциально, на другом может перестать работать как ожидалось (это тоже в каком-то смысле интеграционное тестирование).</p>
20
<p>В действительности же нет никаких четких разделений на три уровня. Даже если вы тестируете чистую функцию (модульное тестирование), она выполняется на конкретном железе, и потенциально, на другом может перестать работать как ожидалось (это тоже в каком-то смысле интеграционное тестирование).</p>
21
<p>Так вот, есть только шкала. Чем более простую и мелкую часть системы мы тестируем - тем дешевле тесты, чем более сложную (составную) - тем дороже. И ваша задача как специалиста - исходить не из того, чтобы соответствовать своим представлениям о видах тестирования, а писать тесты так, чтобы они в идеале покрывали большее число кейсов при небольших затратах. Я уверен, что на этой фразе некоторые разработчики напряглись, потому что в их картине мира нужно обязательно писать изолированные юнит-тесты, а приемочные должны писать только тестировщики. Не буду разводить полемику, просто скажу, что бывает по-разному. Есть проекты, в которых процент юнит-тестов (в самом жестком понимании) составляет доли процента от всех остальных тестов (как в Хекслете, хе-хе), а есть те, где пишут только приемочные тесты (отдельные тестировщики).</p>
21
<p>Так вот, есть только шкала. Чем более простую и мелкую часть системы мы тестируем - тем дешевле тесты, чем более сложную (составную) - тем дороже. И ваша задача как специалиста - исходить не из того, чтобы соответствовать своим представлениям о видах тестирования, а писать тесты так, чтобы они в идеале покрывали большее число кейсов при небольших затратах. Я уверен, что на этой фразе некоторые разработчики напряглись, потому что в их картине мира нужно обязательно писать изолированные юнит-тесты, а приемочные должны писать только тестировщики. Не буду разводить полемику, просто скажу, что бывает по-разному. Есть проекты, в которых процент юнит-тестов (в самом жестком понимании) составляет доли процента от всех остальных тестов (как в Хекслете, хе-хе), а есть те, где пишут только приемочные тесты (отдельные тестировщики).</p>
22
<p>Теперь вы готовы, и я попробую ответить на вопросы, поставленные в начале статьи. Предположим, что вы пишете программу (утилиту командной строки), которая принимает на вход файл и слово, которое нужно найти в этом файле. В результате своей работы программа печатает на экран все строчки из файла, в которых встречается это слово. Такая утилита действительно существует и называется grep. С ней знакомо большинство разработчиков.</p>
22
<p>Теперь вы готовы, и я попробую ответить на вопросы, поставленные в начале статьи. Предположим, что вы пишете программу (утилиту командной строки), которая принимает на вход файл и слово, которое нужно найти в этом файле. В результате своей работы программа печатает на экран все строчки из файла, в которых встречается это слово. Такая утилита действительно существует и называется grep. С ней знакомо большинство разработчиков.</p>
23
<blockquote><h3>Учитесь:</h3>
23
<blockquote><h3>Учитесь:</h3>
24
<p>Курс по<a>автоматическому тестированию</a>на JS, а для опытных программистов -<a>продвинутое</a>.</p>
24
<p>Курс по<a>автоматическому тестированию</a>на JS, а для опытных программистов -<a>продвинутое</a>.</p>
25
</blockquote><p>Обычно в таких программах не всегда сразу понятно, какой будет архитектура. Многое зависит от того, что будет добавлено в процессе, например, форматы вывода, поддерживаемые форматы входа, обход директорий (рекурсивный), нечеткий поиск и многое другое.</p>
25
</blockquote><p>Обычно в таких программах не всегда сразу понятно, какой будет архитектура. Многое зависит от того, что будет добавлено в процессе, например, форматы вывода, поддерживаемые форматы входа, обход директорий (рекурсивный), нечеткий поиск и многое другое.</p>
26
<p>Основной наблюдаемый мной анти-паттерн в разработке подобных библиотек - это тесты на внутренние мелкие компоненты. Те самые юнит-тесты. Почему такой подход непродуктивен? Возможно, это и не очевидно, но такое тестирование, хоть и является модульным, но не является дешевым и качественным. Но, как…?</p>
26
<p>Основной наблюдаемый мной анти-паттерн в разработке подобных библиотек - это тесты на внутренние мелкие компоненты. Те самые юнит-тесты. Почему такой подход непродуктивен? Возможно, это и не очевидно, но такое тестирование, хоть и является модульным, но не является дешевым и качественным. Но, как…?</p>
27
<p>Мы уже говорили о том, что архитектура проекта еще неизвестна, и, как правило, внутреннее разделение на файлы/модули/классы/функции меняется с космической скоростью. В течение часа все может быть переписано несколько раз. Но теперь вместе с кодом нужно постоянно править тесты, что начинает раздражать. Программист начинает сомневаться в том, что они вообще ему нужны, и нередко просто перестает их писать. Другие продолжают мучаться и переписывать их, хотя чаще происходит другое. Написанные тесты начинают вас сковывать и мозг шлет команды "ты потратил время, оставь все как есть". Постепенно рефакторить становится все сложнее и ленивее. Это очень похоже на ситуацию, когда предприниматель инвестировал деньги в новое направление и, даже если бизнес уже тонет, ему тяжело отказаться, ведь было потрачено столько сил и средств (<em>в экономике это называют<a>sunk cost fallacy</a>, - прим. ред.</em>).</p>
27
<p>Мы уже говорили о том, что архитектура проекта еще неизвестна, и, как правило, внутреннее разделение на файлы/модули/классы/функции меняется с космической скоростью. В течение часа все может быть переписано несколько раз. Но теперь вместе с кодом нужно постоянно править тесты, что начинает раздражать. Программист начинает сомневаться в том, что они вообще ему нужны, и нередко просто перестает их писать. Другие продолжают мучаться и переписывать их, хотя чаще происходит другое. Написанные тесты начинают вас сковывать и мозг шлет команды "ты потратил время, оставь все как есть". Постепенно рефакторить становится все сложнее и ленивее. Это очень похоже на ситуацию, когда предприниматель инвестировал деньги в новое направление и, даже если бизнес уже тонет, ему тяжело отказаться, ведь было потрачено столько сил и средств (<em>в экономике это называют<a>sunk cost fallacy</a>, - прим. ред.</em>).</p>
28
<p>Так как же лучше написать тест? Надеюсь, вам уже стало очевидно, что нужно найти достаточно высокую точку входа в нашу программу, которая не зависит от внутренней реализации, и при этом выполняет поставленную задачу.</p>
28
<p>Так как же лучше написать тест? Надеюсь, вам уже стало очевидно, что нужно найти достаточно высокую точку входа в нашу программу, которая не зависит от внутренней реализации, и при этом выполняет поставленную задачу.</p>
29
<p>Если мы попробуем взять самый высокий уровень - прямой запуск программы в консоли - то, скорее всего, мы столкнемся с рядом проблем, такими как запуск отдельного процесса, чтение стандартных потоков и других. В случае нашей программы такое тестирование уже можно называть системным, ведь мы проверяем работу на максимально высоком уровне, вообще не касаясь внутренней реализации. Хотя такой тест и не является проблемой для опытного разработчика, в целом, стоимость подобного теста и для данной библиотеки можно назвать максимальной.</p>
29
<p>Если мы попробуем взять самый высокий уровень - прямой запуск программы в консоли - то, скорее всего, мы столкнемся с рядом проблем, такими как запуск отдельного процесса, чтение стандартных потоков и других. В случае нашей программы такое тестирование уже можно называть системным, ведь мы проверяем работу на максимально высоком уровне, вообще не касаясь внутренней реализации. Хотя такой тест и не является проблемой для опытного разработчика, в целом, стоимость подобного теста и для данной библиотеки можно назвать максимальной.</p>
30
<p>Более низкий уровень - это функция, которая принимает на вход путь до файла и подстроку для поиска, а на выходе (не печатает на экран!) отдает готовый результат, так, чтобы осталось только напечатать его. Такой вид тестов обладает самым лучшим балансом "убедиться в том, что все работает/стоимость". Они косвенно затрагивают все используемые внутренности, не зависят от реализации, очень просты в написании и крайне дешевы в поддержке. По мере стабилизации архитектуры можно добавлять тесты более низкого уровня (если становится понятно, что сложность системы слишком высока).</p>
30
<p>Более низкий уровень - это функция, которая принимает на вход путь до файла и подстроку для поиска, а на выходе (не печатает на экран!) отдает готовый результат, так, чтобы осталось только напечатать его. Такой вид тестов обладает самым лучшим балансом "убедиться в том, что все работает/стоимость". Они косвенно затрагивают все используемые внутренности, не зависят от реализации, очень просты в написании и крайне дешевы в поддержке. По мере стабилизации архитектуры можно добавлять тесты более низкого уровня (если становится понятно, что сложность системы слишком высока).</p>
31
<blockquote><h3>Полезно узнать:</h3>
31
<blockquote><h3>Полезно узнать:</h3>
32
<p><a>Чек-лист</a>по тестированию веб-форм</p>
32
<p><a>Чек-лист</a>по тестированию веб-форм</p>
33
</blockquote><h3>TDD</h3>
33
</blockquote><h3>TDD</h3>
34
<p>Описанная методика особенно хорошо работает в связке с подходом, когда тесты пишутся до кода (вместе с кодом).</p>
34
<p>Описанная методика особенно хорошо работает в связке с подходом, когда тесты пишутся до кода (вместе с кодом).</p>
35
-
<p>Существует миф о том, что тесты нужны только для регресса, то есть для уверенности, что новый код не сломал старый. Это далеко не так. Более того, это следствие написания тестов как таковых. В некоторых ситуациях первостепенная цель написания тестов - это ускорение разработки. Да-да, вы не ослышались, написание тестов до кода/одновременно с кодом, приводит к серьезному ускорению разработки. Чаще всего такие ситуации связаны с тем, что на вход подаются сложные данные, которые как-то трансформируются и прокидываются дальше. Тестировать руками (во время разработки) такой код очень сложно, нужно подготавливать данные, нужно проверять, что результат соответствует ожидаемому.</p>
35
+
<p>Существует миф о том, что тесты нужны только для регресса, то есть для уверенности, что новый код не сломал старый. Это далеко не так. Более того, это следствие написания тестов как таковых. В некоторых ситуациях первостепенная цель написания тестов - это ускорение разработки. Да-да, вы не ослышались, написание тестов до кода/одновременно с кодом, приводит к серьезному уск��рению разработки. Чаще всего такие ситуации связаны с тем, что на вход подаются сложные данные, которые как-то трансформируются и прокидываются дальше. Тестировать руками (во время разработки) такой код очень сложно, нужно подготавливать данные, нужно проверять, что результат соответствует ожидаемому.</p>
36
<p>Важно понимать, что ускорение возможно только после того, как вы наберетесь опыта и начнете чувствовать себя уверенно в мире автоматизированного тестирования. К тому же, есть виды тестирования, где писать тесты до кода сложно либо практически невозможно. К таким тестам, например, относится приемное тестирование через браузер.</p>
36
<p>Важно понимать, что ускорение возможно только после того, как вы наберетесь опыта и начнете чувствовать себя уверенно в мире автоматизированного тестирования. К тому же, есть виды тестирования, где писать тесты до кода сложно либо практически невозможно. К таким тестам, например, относится приемное тестирование через браузер.</p>
37
<p>Второе серьезное преимущество TDD заключается в том, что при проектировании кода мы начинаем думать не о том, как сейчас клево насоздаем файлов и разнесем по ним функции, создав десятки абстракций, и начнем думать о важных вещах: о том, как будет использоваться моя библиотека. Удивительно, но начать смотреть с такого угла (а этому учат всех стартаперов, customer development во все поля) непросто, все время хочется окунуться в прекрасный мир архитектуры.</p>
37
<p>Второе серьезное преимущество TDD заключается в том, что при проектировании кода мы начинаем думать не о том, как сейчас клево насоздаем файлов и разнесем по ним функции, создав десятки абстракций, и начнем думать о важных вещах: о том, как будет использоваться моя библиотека. Удивительно, но начать смотреть с такого угла (а этому учат всех стартаперов, customer development во все поля) непросто, все время хочется окунуться в прекрасный мир архитектуры.</p>
38
<p>Проектирование внешнего api - действительно важная задача, которой стоит уделить хотя бы немного времени. Именно она больше всего влияет на ту свободу действий при переработке, которую вы получите в будущем. И именно этот уровень чаще всего является той точкой, от которой стоит отталкиваться при формировании планов тестирования.</p>
38
<p>Проектирование внешнего api - действительно важная задача, которой стоит уделить хотя бы немного времени. Именно она больше всего влияет на ту свободу действий при переработке, которую вы получите в будущем. И именно этот уровень чаще всего является той точкой, от которой стоит отталкиваться при формировании планов тестирования.</p>
39
<h3>Дополнительные ссылки</h3>
39
<h3>Дополнительные ссылки</h3>
40
<ul><li><a>Видео версия статьи</a></li>
40
<ul><li><a>Видео версия статьи</a></li>
41
<li><a>Бережливое тестирование</a></li>
41
<li><a>Бережливое тестирование</a></li>
42
</ul>
42
</ul>