HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-26
1 <p>Property-based тестирование (тестирование, основанное на свойствах) - подход к функциональному тестированию, который помогает проверить, соответствует ли тестируемая функция заданному свойству. Для этого подхода не нужно задавать все тестовые примеры. Его задача - сопоставлять характеристики на выходе с заданным свойством.</p>
1 <p>Property-based тестирование (тестирование, основанное на свойствах) - подход к функциональному тестированию, который помогает проверить, соответствует ли тестируемая функция заданному свойству. Для этого подхода не нужно задавать все тестовые примеры. Его задача - сопоставлять характеристики на выходе с заданным свойством.</p>
2 <p>Свойство - это утверждение, которое в виде псевдокода можно представить так:</p>
2 <p>Свойство - это утверждение, которое в виде псевдокода можно представить так:</p>
3 <p>for all (x, y, ...) such as precondition(x, y, ...) holds property(x, y, ...) is true</p>
3 <p>for all (x, y, ...) such as precondition(x, y, ...) holds property(x, y, ...) is true</p>
4 <p>Мы описываем инвариант в стиле "для любых данных, таких, что ... выполняется условие ..." и, в отличие от обычных тестов, не задаем явно все тестовые примеры, а только описываем условия, которым они должны удовлетворять.</p>
4 <p>Мы описываем инвариант в стиле "для любых данных, таких, что ... выполняется условие ..." и, в отличие от обычных тестов, не задаем явно все тестовые примеры, а только описываем условия, которым они должны удовлетворять.</p>
5 <p>Предположим, у нас есть функция divide(), которая находит частное двух чисел.</p>
5 <p>Предположим, у нас есть функция divide(), которая находит частное двух чисел.</p>
6 <p>Напишем обычный тест на эту функцию:</p>
6 <p>Напишем обычный тест на эту функцию:</p>
7 <p>Здесь все просто, передаем на вход числа 18 и 3, ожидаем получить 6. Тесты проходят. Но здесь мы проверили работу функции только на двух парах входных данных. Тест показывает, что функция отрабатывает верно только в этих двух случаях. Но может оказаться так, что при другой паре чисел функция работает неожиданно. Чтобы решить эту проблему, нам нужно написать тест, который сфокусируется не на входных и выходных параметрах, а на свойствах в целом. Эти свойства должны быть истинными для любой правильной реализации.</p>
7 <p>Здесь все просто, передаем на вход числа 18 и 3, ожидаем получить 6. Тесты проходят. Но здесь мы проверили работу функции только на двух парах входных данных. Тест показывает, что функция отрабатывает верно только в этих двух случаях. Но может оказаться так, что при другой паре чисел функция работает неожиданно. Чтобы решить эту проблему, нам нужно написать тест, который сфокусируется не на входных и выходных параметрах, а на свойствах в целом. Эти свойства должны быть истинными для любой правильной реализации.</p>
8 <p>У операции деления есть свойство:<a>дистрибутивность справа</a>. Оно означает, что деление суммы двух чисел a и b на число c равно сумме a / c + b / c.</p>
8 <p>У операции деления есть свойство:<a>дистрибутивность справа</a>. Оно означает, что деление суммы двух чисел a и b на число c равно сумме a / c + b / c.</p>
9 <p>Используем это в тестах. Не будем завязываться на конкретные значения и для получения тестовых данных используем генератор случайных чисел.</p>
9 <p>Используем это в тестах. Не будем завязываться на конкретные значения и для получения тестовых данных используем генератор случайных чисел.</p>
10 <p>Будем запускать этот тест много раз в цикле, и в определенный момент получим такую комбинацию тестовых данных, когда все три числа равны нулю. Тест упадет, так как деление нуля на нуль дает NaN, а NaN не равен NaN. Так мы понимаем, что в функцию нужно добавить проверку на нули.</p>
10 <p>Будем запускать этот тест много раз в цикле, и в определенный момент получим такую комбинацию тестовых данных, когда все три числа равны нулю. Тест упадет, так как деление нуля на нуль дает NaN, а NaN не равен NaN. Так мы понимаем, что в функцию нужно добавить проверку на нули.</p>
11 <p>Мы написали обычный тест, но использовали в нем не взятые из головы, а произвольные значения и получили возможность выполнять тест много раз на разных входных данных. Таким образом мы проверили саму спецификацию (то, что функция должна делать), а не ее поведение в отдельных случаях. Это и есть тестирование на основе свойств - property-based testing.</p>
11 <p>Мы написали обычный тест, но использовали в нем не взятые из головы, а произвольные значения и получили возможность выполнять тест много раз на разных входных данных. Таким образом мы проверили саму спецификацию (то, что функция должна делать), а не ее поведение в отдельных случаях. Это и есть тестирование на основе свойств - property-based testing.</p>
12 <p>В реальной жизни никто не гоняет тесты в цикле, подставляя туда значения вручную. Для этого есть готовые фреймворки. Данные генерируются автоматически фреймворком для property-based тестирования на основании описанных свойств. Если после определенного числа прогонов со случайными данными, удовлетворяющими описанию, условие выполняется, тест считается пройденным. Иначе фреймворк завершает тест с ошибкой.</p>
12 <p>В реальной жизни никто не гоняет тесты в цикле, подставляя туда значения вручную. Для этого есть готовые фреймворки. Данные генерируются автоматически фреймворком для property-based тестирования на основании описанных свойств. Если после определенного числа прогонов со случайными данными, удовлетворяющими описанию, условие выполняется, тест считается пройденным. Иначе фреймворк завершает тест с ошибкой.</p>
13 <p>Рассмотрим, в чем заключаются преимущества property-based тестирования:</p>
13 <p>Рассмотрим, в чем заключаются преимущества property-based тестирования:</p>
14 <ul><li><p>Охватывает все возможные данные. Фреймворки автоматически генерируют данные на основании описанных свойств. В теории эта особенность позволяет охватить все возможные типы входных данных: например, весь диапазон строк или целых чисел</p>
14 <ul><li><p>Охватывает все возможные данные. Фреймворки автоматически генерируют данные на основании описанных свойств. В теории эта особенность позволяет охватить все возможные типы входных данных: например, весь диапазон строк или целых чисел</p>
15 </li>
15 </li>
16 <li><p>Сокращает тестовый пример в случае сбоя: всякий раз, когда происходит сбой, фреймворк пытается сократить тестовый пример. Например, если условием сбоя является наличие заданного символа в строке, фреймворк должен возвращать строку из одного символа, которая содержит только этот символ. Это серьезное преимущество property-тестирования - в случае сбоя тест прекращает работу на минимальном примере, а не на наборе входных данных</p>
16 <li><p>Сокращает тестовый пример в случае сбоя: всякий раз, когда происходит сбой, фреймворк пытается сократить тестовый пример. Например, если условием сбоя является наличие заданного символа в строке, фреймворк должен возвращать строку из одного символа, которая содержит только этот символ. Это серьезное преимущество property-тестирования - в случае сбоя тест прекращает работу на минимальном примере, а не на наборе входных данных</p>
17 </li>
17 </li>
18 <li><p>Воспроизводимость: перед каждым запуском теста создаются начальные значения, благодаря которым в случае сбоя можно воспроизвести проверку на том же наборе данных</p>
18 <li><p>Воспроизводимость: перед каждым запуском теста создаются начальные значения, благодаря которым в случае сбоя можно воспроизвести проверку на том же наборе данных</p>
19 </li>
19 </li>
20 </ul><p>Важно отметить, что property-тестирование не заменяет модульного. К нему нужно относиться как к дополнительному уровню тестов, который поможет сократить время на проверку корректности работы кода по сравнению с другими подходами.</p>
20 </ul><p>Важно отметить, что property-тестирование не заменяет модульного. К нему нужно относиться как к дополнительному уровню тестов, который поможет сократить время на проверку корректности работы кода по сравнению с другими подходами.</p>
21 <h2>Фреймворки</h2>
21 <h2>Фреймворки</h2>
22 <p>Идея property-тестирования была впервые реализована во фреймворке QuickCheck в языке Haskell. Для JavaScript тоже есть несколько библиотек, одна из них<a>fast-check</a>.</p>
22 <p>Идея property-тестирования была впервые реализована во фреймворке QuickCheck в языке Haskell. Для JavaScript тоже есть несколько библиотек, одна из них<a>fast-check</a>.</p>
23 <p>Для ее установки нужно выполнить команду:</p>
23 <p>Для ее установки нужно выполнить команду:</p>
24 <p>Протестируем с ее помощью реализацию функции contains(), которая проверяет, содержится ли подстрока в строке. У строк можно выделить два свойства, которые мы можем использовать:</p>
24 <p>Протестируем с ее помощью реализацию функции contains(), которая проверяет, содержится ли подстрока в строке. У строк можно выделить два свойства, которые мы можем использовать:</p>
25 <ul><li>Строка всегда содержит саму себя в качестве подстроки</li>
25 <ul><li>Строка всегда содержит саму себя в качестве подстроки</li>
26 <li>Строка a + b + c всегда содержит свою подстроку b, независимо от содержания a, b и c</li>
26 <li>Строка a + b + c всегда содержит свою подстроку b, независимо от содержания a, b и c</li>
27 </ul><p>Разберем структуру теста подробнее</p>
27 </ul><p>Разберем структуру теста подробнее</p>
28 <p>fc.assert(&lt;property&gt;(, parameters)) - выполняет тестирование и проверяет, что свойство остается верным для всех созданных библиотекой строк a, b и c. Когда происходит сбой, эта строка отвечает за сокращение тестового примера до минимального размера, чтобы упростить задачу пользователю. По умолчанию он выполняет проверку свойств по 100 сгенерированным входным данным.</p>
28 <p>fc.assert(&lt;property&gt;(, parameters)) - выполняет тестирование и проверяет, что свойство остается верным для всех созданных библиотекой строк a, b и c. Когда происходит сбой, эта строка отвечает за сокращение тестового примера до минимального размера, чтобы упростить задачу пользователю. По умолчанию он выполняет проверку свойств по 100 сгенерированным входным данным.</p>
29 <p>fc.property(&lt;...arbitraries&gt;, &lt;predicate&gt;) - описывает свойство. arbitraries - это значения, которые отвечают за построение входных данных, а predicate - это функция, которая проверяет входные данные. predicate должен либо возвращать логическое значение, либо не возвращать ничего и завершать тест в случае сбоя.</p>
29 <p>fc.property(&lt;...arbitraries&gt;, &lt;predicate&gt;) - описывает свойство. arbitraries - это значения, которые отвечают за построение входных данных, а predicate - это функция, которая проверяет входные данные. predicate должен либо возвращать логическое значение, либо не возвращать ничего и завершать тест в случае сбоя.</p>
30 <p>fc.string() - генератор строк, который отвечает за создание и сокращение тестовых значений.</p>
30 <p>fc.string() - генератор строк, который отвечает за создание и сокращение тестовых значений.</p>
31 <p>При желании можно извлечь сгенерированные значения для проверки свойств, заменив fc.assert на fc.sample:</p>
31 <p>При желании можно извлечь сгенерированные значения для проверки свойств, заменив fc.assert на fc.sample:</p>
32 <p>Сгенерированные данные будут выглядеть примерно так:</p>
32 <p>Сгенерированные данные будут выглядеть примерно так:</p>
33 <p>Теперь попробуем протестировать заведомо неправильную реализацию функции contains(). Используем ее в качестве примера, чтобы показать, что фреймворк генерирует в случае сбоя и как он сокращает ввод:</p>
33 <p>Теперь попробуем протестировать заведомо неправильную реализацию функции contains(). Используем ее в качестве примера, чтобы показать, что фреймворк генерирует в случае сбоя и как он сокращает ввод:</p>
34 <p>Фреймворк генерирует определенный набор данных. Как только тест видит сбой, он запускает процесс сокращения. При тестировании примера, приведенного выше, происходит сбой:</p>
34 <p>Фреймворк генерирует определенный набор данных. Как только тест видит сбой, он запускает процесс сокращения. При тестировании примера, приведенного выше, происходит сбой:</p>
35 <p>Теперь рассмотрим другой вариант реализации, когда predicate не будет возвращать логическое значение, а в случае возникновения ошибки будет завершать тест. Поменяем реализацию тестирования функции contains() соответственно:</p>
35 <p>Теперь рассмотрим другой вариант реализации, когда predicate не будет возвращать логическое значение, а в случае возникновения ошибки будет завершать тест. Поменяем реализацию тестирования функции contains() соответственно:</p>
36 <h2>Заключение</h2>
36 <h2>Заключение</h2>
37 <p>Тестирование на основе свойств - полезный и мощный инструмент. Мы не должны отказываться от классических тестов, но можем их комбинировать с тестированием на основе свойств. Например, можно базовый функционал покрывать классическими тестами на основе примеров, а критически важные функции дополнительно покрывать property-тестами.</p>
37 <p>Тестирование на основе свойств - полезный и мощный инструмент. Мы не должны отказываться от классических тестов, но можем их комбинировать с тестированием на основе свойств. Например, можно базовый функционал покрывать классическими тестами на основе примеров, а критически важные функции дополнительно покрывать property-тестами.</p>