0 added
2 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Какую главную задачу должны решать тесты? Этот вопрос невероятно важен. Ответ на него дает понимание того, как правильно писать тесты и как писать их не нужно.</p>
1
<p>Какую главную задачу должны решать тесты? Этот вопрос невероятно важен. Ответ на него дает понимание того, как правильно писать тесты и как писать их не нужно.</p>
2
<p>Представьте, что вы написали функцию capitalize($text), которая делает заглавной первую букву переданной строки:</p>
2
<p>Представьте, что вы написали функцию capitalize($text), которая делает заглавной первую букву переданной строки:</p>
3
<p>Вот один из вариантов ее реализации:</p>
3
<p>Вот один из вариантов ее реализации:</p>
4
<p>Что мы делаем после создания функции? Проверяем, как она работает. Например, открываем REPL и вызываем функцию с разными аргументами:</p>
4
<p>Что мы делаем после создания функции? Проверяем, как она работает. Например, открываем REPL и вызываем функцию с разными аргументами:</p>
5
<p>Таким нехитрым способом убеждаемся, что функция работает. По крайней мере для тех аргументов, которые мы передали в нее. Если во время проверки заметили ошибки, то исправляем функцию и повторяем все заново.</p>
5
<p>Таким нехитрым способом убеждаемся, что функция работает. По крайней мере для тех аргументов, которые мы передали в нее. Если во время проверки заметили ошибки, то исправляем функцию и повторяем все заново.</p>
6
<p>Фактически, весь этот процесс - это и есть тестирование, но не автоматическое, а<strong>ручное</strong>. Задача такого тестирования - убедиться, что код работает, как надо. В этом случае нам без разницы, как конкретно реализована эта функция.</p>
6
<p>Фактически, весь этот процесс - это и есть тестирование, но не автоматическое, а<strong>ручное</strong>. Задача такого тестирования - убедиться, что код работает, как надо. В этом случае нам без разницы, как конкретно реализована эта функция.</p>
7
<p>Теперь мы можем ответить на главный вопрос, заданный в начале урока:</p>
7
<p>Теперь мы можем ответить на главный вопрос, заданный в начале урока:</p>
8
<blockquote><p>Тесты проверяют, что код или приложение работает корректно. Они не заботятся о том, как конкретно написан код, который они проверяют.</p>
8
<blockquote><p>Тесты проверяют, что код или приложение работает корректно. Они не заботятся о том, как конкретно написан код, который они проверяют.</p>
9
</blockquote><h2>Что такое автоматические тесты</h2>
9
</blockquote><h2>Что такое автоматические тесты</h2>
10
<p>Все, что требуется от автоматических тестов - повторить проверки, которые мы выполняли при ручном тестировании. Для этого достаточно старого доброго if и исключений.</p>
10
<p>Все, что требуется от автоматических тестов - повторить проверки, которые мы выполняли при ручном тестировании. Для этого достаточно старого доброго if и исключений.</p>
11
<p>Даже если вы не знакомы с исключениями, ничего страшного. В этом курсе достаточно знать две вещи: для чего они нам нужны и какой у них синтаксис.</p>
11
<p>Даже если вы не знакомы с исключениями, ничего страшного. В этом курсе достаточно знать две вещи: для чего они нам нужны и какой у них синтаксис.</p>
12
<p>До сих пор в курсах Хекслета вы встречались с ошибками, которые возникают непроизвольно: вызов несуществующей функции, обращение к несуществующей переменной и так далее. Но ошибки можно порождать самостоятельно с помощью исключений, что необходимо для нашей ситуации. Исключения создаются такой конструкцией:</p>
12
<p>До сих пор в курсах Хекслета вы встречались с ошибками, которые возникают непроизвольно: вызов несуществующей функции, обращение к несуществующей переменной и так далее. Но ошибки можно порождать самостоятельно с помощью исключений, что необходимо для нашей ситуации. Исключения создаются такой конструкцией:</p>
13
<p>Пример теста:</p>
13
<p>Пример теста:</p>
14
<p>Из примера выше видно, что тесты - это точно такой же код, как и любой другой. Он работает в том же окружении и подчиняется тем же правилам - например, стандартам кодирования. А еще он может содержать ошибки. Но это не значит, что надо писать тесты на тесты. Если бы мы стремились избавиться от всех ошибок, стоимость разработки стала бы неоправданно высокой. Обнаруженные ошибки в тестах исправляются, и жизнь продолжается дальше ;)</p>
14
<p>Из примера выше видно, что тесты - это точно такой же код, как и любой другой. Он работает в том же окружении и подчиняется тем же правилам - например, стандартам кодирования. А еще он может содержать ошибки. Но это не значит, что надо писать тесты на тесты. Если бы мы стремились избавиться от всех ошибок, стоимость разработки стала бы неоправданно высокой. Обнаруженные ошибки в тестах исправляются, и жизнь продолжается дальше ;)</p>
15
<p>Как правило, тесты складывают в специальную директорию в корне проекта. Обычно она называется<em>tests</em>, хотя встречаются и другие варианты:</p>
15
<p>Как правило, тесты складывают в специальную директорию в корне проекта. Обычно она называется<em>tests</em>, хотя встречаются и другие варианты:</p>
16
-
<p>bin └── php-package src/ └── User.php tests/ └── UserTest.php</p>
17
<p>Структура этой директории зависит от того, на основе какого фреймворка пишутся тесты. В простых случаях она отражает структуру исходного кода. Если предположить, что наша функция capitalize($text) определена в файле<em>src/StringUtils.php</em>, то ее тест лучше поместить в файл<em>tests/StringUtilsTest.php</em>. Слово<em>Test</em>в имени модуля с тестами используется только для более явного обозначения цели файла.</p>
16
<p>Структура этой директории зависит от того, на основе какого фреймворка пишутся тесты. В простых случаях она отражает структуру исходного кода. Если предположить, что наша функция capitalize($text) определена в файле<em>src/StringUtils.php</em>, то ее тест лучше поместить в файл<em>tests/StringUtilsTest.php</em>. Слово<em>Test</em>в имени модуля с тестами используется только для более явного обозначения цели файла.</p>
18
<p>Теперь при любых изменениях, затрагивающих эту функцию, важно не забывать запускать тесты:</p>
17
<p>Теперь при любых изменениях, затрагивающих эту функцию, важно не забывать запускать тесты:</p>
19
<h2>Как пишутся тесты</h2>
18
<h2>Как пишутся тесты</h2>
20
<p>Тесты - это не магия. Разработчикам нужно самостоятельно импортировать тестируемые функции, вызывать их с необходимыми аргументами и проверять, что функции возвращают ожидаемые значения.</p>
19
<p>Тесты - это не магия. Разработчикам нужно самостоятельно импортировать тестируемые функции, вызывать их с необходимыми аргументами и проверять, что функции возвращают ожидаемые значения.</p>
21
<p>Для примера представим, что у нас поменялась сигнатура функции - входные или выходные параметры, ее имя. В таком случае придется переписывать тесты. Рассмотрим, что будет, если сигнатура осталась той же, но поменялись внутренности функции:</p>
20
<p>Для примера представим, что у нас поменялась сигнатура функции - входные или выходные параметры, ее имя. В таком случае придется переписывать тесты. Рассмотрим, что будет, если сигнатура осталась той же, но поменялись внутренности функции:</p>
22
<p>Тогда тесты должны продолжать работать без изменений. Хорошие тесты ничего не знают про внутреннее устройство проверяемого кода. Это делает их более универсальными и надежными.</p>
21
<p>Тогда тесты должны продолжать работать без изменений. Хорошие тесты ничего не знают про внутреннее устройство проверяемого кода. Это делает их более универсальными и надежными.</p>
23
<h3>Количество проверок</h3>
22
<h3>Количество проверок</h3>
24
<p>Невозможно написать тесты, которые гарантируют стопроцентную работоспособность кода. Для этого пришлось бы реализовать проверки всех возможных аргументов, что физически невозможно. С другой стороны, без тестов вообще нет никаких гарантий, что код будет работать.</p>
23
<p>Невозможно написать тесты, которые гарантируют стопроцентную работоспособность кода. Для этого пришлось бы реализовать проверки всех возможных аргументов, что физически невозможно. С другой стороны, без тестов вообще нет никаких гарантий, что код будет работать.</p>
25
<p>При написании тестов нужно ориентироваться на разнообразие входных данных. У любой функции есть один или несколько основных сценариев использования. Например, в случае capitalize() - это любое слово. Достаточно написать ровно одну проверку, которая покрывает этот сценарий. Дальше нужно смотреть на<strong>пограничные случаи</strong>. Это ситуации, в которых код может повести себя по-особенному:</p>
24
<p>При написании тестов нужно ориентироваться на разнообразие входных данных. У любой функции есть один или несколько основных сценариев использования. Например, в случае capitalize() - это любое слово. Достаточно написать ровно одну проверку, которая покрывает этот сценарий. Дальше нужно смотреть на<strong>пограничные случаи</strong>. Это ситуации, в которых код может повести себя по-особенному:</p>
26
<ul><li>Работа с пустой строкой</li>
25
<ul><li>Работа с пустой строкой</li>
27
<li>Обработка null</li>
26
<li>Обработка null</li>
28
<li>Деление на ноль (потому что в большинстве языков оно вызывает ошибку)</li>
27
<li>Деление на ноль (потому что в большинстве языков оно вызывает ошибку)</li>
29
<li>Специфические ситуации для конкретных алгоритмов</li>
28
<li>Специфические ситуации для конкретных алгоритмов</li>
30
</ul><p>Для capitalize() пограничным случаем будет пустая строка:</p>
29
</ul><p>Для capitalize() пограничным случаем будет пустая строка:</p>
31
<p>Добавив тест на пустую строку, мы увидим, что вызов функции capitalize() завершается с ошибкой. Внутри нее идет обращение к первому индексу строки без проверки его существования. Посмотрим на исправленную версию кода:</p>
30
<p>Добавив тест на пустую строку, мы увидим, что вызов функции capitalize() завершается с ошибкой. Внутри нее идет обращение к первому индексу строки без проверки его существования. Посмотрим на исправленную версию кода:</p>
32
<p>Обычно пограничные случаи требуют отдельной обработки и наличия условных конструкций. Тесты должны строиться так, чтобы они затрагивали каждую такую конструкцию. Но не забывайте, что условные конструкции могут порождать хитрые связи.</p>
31
<p>Обычно пограничные случаи требуют отдельной обработки и наличия условных конструкций. Тесты должны строиться так, чтобы они затрагивали каждую такую конструкцию. Но не забывайте, что условные конструкции могут порождать хитрые связи.</p>
33
<p>Например, функция из двух независимых условных блоков может выполниться:</p>
32
<p>Например, функция из двух независимых условных блоков может выполниться:</p>
34
<ul><li>Так, что не выполнился ни один условный блок</li>
33
<ul><li>Так, что не выполнился ни один условный блок</li>
35
<li>Так, что выполнился только первый условный блок</li>
34
<li>Так, что выполнился только первый условный блок</li>
36
<li>Так, что выполнился только второй условный блок</li>
35
<li>Так, что выполнился только второй условный блок</li>
37
<li>Так, что выполнились оба условных блока</li>
36
<li>Так, что выполнились оба условных блока</li>
38
</ul><p>Комбинация всех возможных вариантов поведения функции называется<strong>цикломатической сложностью</strong>. Это число показывает все возможные пути выполнения программы внутри функции. Цикломатическая сложность - хороший ориентир для понимания того, сколько и какие тесты нужно написать.</p>
37
</ul><p>Комбинация всех возможных вариантов поведения функции называется<strong>цикломатической сложностью</strong>. Это число показывает все возможные пути выполнения программы внутри функции. Цикломатическая сложность - хороший ориентир для понимания того, сколько и какие тесты нужно написать.</p>
39
<p>Иногда пограничные случаи не связаны с условными конструкциями. Особенно часто такие ситуации встречаются там, где есть вычисления границ слов или массивов. Такой код может работать в большинстве ситуаций, но только в некоторых может давать сбой:</p>
38
<p>Иногда пограничные случаи не связаны с условными конструкциями. Особенно часто такие ситуации встречаются там, где есть вычисления границ слов или массивов. Такой код может работать в большинстве ситуаций, но только в некоторых может давать сбой:</p>
40
<h3>Проверка входных данных</h3>
39
<h3>Проверка входных данных</h3>
41
<p>Отдельно рассмотрим ошибки типов входных данных. Например, в функцию capitalize() можно передать число вместо строки. Как она должна себя вести в таком случае? Нужно ли писать такой тест?</p>
40
<p>Отдельно рассмотрим ошибки типов входных данных. Например, в функцию capitalize() можно передать число вместо строки. Как она должна себя вести в таком случае? Нужно ли писать такой тест?</p>
42
<p>Есть еще один интересный вопрос. Нужно ли внутри capitalize() обрабатывать такие ситуации? Ответ - не нужно. Иначе код превратится в мусорку, а пользы от этого мало. Все равно должны быть тесты, которые проверяют, что система работает в целом, а они обычно выявляют проблемы кода на более нижних уровнях.</p>
41
<p>Есть еще один интересный вопрос. Нужно ли внутри capitalize() обрабатывать такие ситуации? Ответ - не нужно. Иначе код превратится в мусорку, а пользы от этого мало. Все равно должны быть тесты, которые проверяют, что система работает в целом, а они обычно выявляют проблемы кода на более нижних уровнях.</p>
43
<p>Ответственность за передачу правильных данных в функцию capitalize() лежит не на самой функции, а на коде, который вызывает ее. Если он хорошо протестирован, то подобная ошибка либо обнаружится, либо вообще не возникнет.</p>
42
<p>Ответственность за передачу правильных данных в функцию capitalize() лежит не на самой функции, а на коде, который вызывает ее. Если он хорошо протестирован, то подобная ошибка либо обнаружится, либо вообще не возникнет.</p>
44
<p>Даже если ошибка обрабатывается внутри функции, не надо пытаться написать тесты, покрывающие каждую ошибку. Это выливается в огромное число тестов, которые требуют поддержки и времени на написание. Нужно уметь вовремя остановиться и двигаться дальше, к покрытию другого кода.</p>
43
<p>Даже если ошибка обрабатывается внутри функции, не надо пытаться написать тесты, покрывающие каждую ошибку. Это выливается в огромное число тестов, которые требуют поддержки и времени на написание. Нужно уметь вовремя остановиться и двигаться дальше, к покрытию другого кода.</p>
45
<h2>Собирая все вместе</h2>
44
<h2>Собирая все вместе</h2>
46
<p>В конечном итоге мы получили такую структуру директорий:</p>
45
<p>В конечном итоге мы получили такую структуру директорий:</p>
47
-
<p>src/ └── StringUtils.php tests/ └──StringUtilsTest.php</p>
48
<p>Содержимое теста:</p>
46
<p>Содержимое теста:</p>
49
<p>Запуск:</p>
47
<p>Запуск:</p>
50
<p>Если все написано правильно, то запуск тестов завершится с выводом строки<em>Все тесты пройдены!</em>. Если в тестах или в коде есть ошибка, то сработает исключение, и мы увидим сообщение, указывающее на это.</p>
48
<p>Если все написано правильно, то запуск тестов завершится с выводом строки<em>Все тесты пройдены!</em>. Если в тестах или в коде есть ошибка, то сработает исключение, и мы увидим сообщение, указывающее на это.</p>