Как проводить мутационное тестирование с помощью Cosmic-Ray
Мутационное тестирование важно для высокого качества кода, в этой статье я расскажу о теории мутационного тестирования, зачем оно нужно и как это сделать с помощью замечательного инструмента под названием Cosmic-Ray.
Строительство с нуля
Приложение
Чтобы понять необходимость тестирования мутаций, давайте начнем с написания калькулятора налоговых ставок.
В этом примере у нас есть прогрессивная налоговая ставка, которая увеличивается с каждой скобкой.
╔═══════════╦══════════════╗ ║ bracket ║ tax rate ║ ╠═══════════╬══════════════║ ║ 4000 ║ 0% ║ ║ 7000 ║ 10% ║ ║ 10000 ║ 20% ║ ║ 15000 ║ 30% ║ ║ 20000 ║ 45% ║ ║ 25000 ║ 50% ║ ║ 30000 ║ 60% ║ ║ 35000 ║ 65% ║ ╚═══════════╩══════════════╝
Если кто-то зарабатывает менее 4000 универсальных кредитов места, он платит 0 налогов.
За каждый 1 USC, заработанный от 4000 до 7000, он должен платить 10 % налогов.
И так далее.
Например, для человека, который зарабатывает 14 000 долларов США:
Необработанный доход = 14 000
4000 * 0% = 0
+
(7000–4000)*10% = 300
+
(10000–7000)*20% = 600
+
(14000–10000)*30%=1200
Общий налоговый платеж = 0 + 300 + 600 + 1200 = 2100
Чистый доход = Необработанный доход — Общий налоговый платеж = 14 000–2100 = 11 900
В дополнение к этому, политики решили стимулировать \ компенсировать людей в зависимости от их возраста и количества детей, которые у них есть.
Налоговая компенсация по возрасту:
╔═══════════╦══════════════╗ ║ age ║ Compensation ║ ╠═══════════╬══════════════║ ║ <20 ║ 100 ║ ║ >20<65 ║ 0 ║ ║ >65<75 ║ 100 ║ ║ >75 ║ 200 ║ ╚═══════════╩══════════════╝
Налоговая компенсация по количеству детей:
╔═══════════╦══════════════╗ ║ # ║ Compensation ║ ╠═══════════╬══════════════║ ║ 0 ║ 0 ║ ║ 1 ║ 50 ║ ║ 2 ║ 80 ║ ║ >2 ║ 100 ║ ╚═══════════╩══════════════╝
calculate_net_incomeпринимает 3 параметра:
raw_income
date_of_birth
number_of_children
pip install python-dateutil # for easy date calculations
"""calculation module """ | |
from datetime import datetime | |
from typing import Dict | |
from dateutil.relativedelta import relativedelta | |
def calculate_net_income( | |
raw_income: float, | |
date_of_birth: datetime, | |
number_of_children: int, | |
) -> float: | |
"""calculate net income after tax reduction and transfer payments. | |
Args: | |
raw_income (float): income before taxes and transfers | |
date_of_birth (datetime): | |
number_of_children (int): | |
Returns: | |
float: disposable tax rate | |
""" | |
if raw_income < 0: | |
raise ValueError("net income cannot be negative!") | |
if number_of_children < 0: | |
raise ValueError("number of children cannot be negative!") | |
# region1 - find basal tax rate | |
progression_steps: Dict[float, int] = { | |
4000: 0, | |
7000: 10, | |
10000: 20, | |
15000: 30, | |
20000: 45, | |
25000: 50, | |
30000: 60, | |
35000: 65, | |
} | |
total_reduction: float = 0 | |
previous_step: float = 0 | |
for floor_value, reduction in progression_steps.items(): | |
if raw_income < floor_value: | |
margin = raw_income - previous_step | |
total_reduction += (reduction / 100) * margin | |
break | |
margin = floor_value - previous_step | |
total_reduction += (reduction / 100) * margin | |
previous_step = floor_value | |
ans: float = raw_income - total_reduction | |
# region 2 - refunds | |
if ans < raw_income: | |
# region 2.1 - refund by age | |
age = relativedelta(datetime.now(), date_of_birth) | |
if age.years < 20: | |
ans += 100 | |
elif age.years > 75: | |
ans += 200 | |
elif age.years > 65: | |
ans += 100 | |
# region 2.2 - refund by # of children | |
if number_of_children == 1: | |
ans += 50 | |
elif number_of_children == 2: | |
ans += 80 | |
elif number_of_children >= 3: | |
ans += 100 | |
return min(ans, raw_income) |
Функция использует правила для расчета чистого дохода человека на основе этих характеристик.
Да, я знаю, это уродливый код.
И я знаю, что многие из вас могут придумать способ получше. этот.
Помните, что это не статья о чистом коде, это статья о мутационных тестах ;)
Пишите модульные тесты
Подготовив наш код, мы можем написать несколько модульных тестов.
Модульные тесты могут помочь нам в будущем с уверенностью реорганизовать наш код.
""" | |
unit tests for calculation modules | |
""" | |
from unittest import TestCase | |
from datetime import datetime | |
from calculations import calculate_net_income | |
class TestFetchLogs(TestCase): | |
"""unit test container class""" | |
def test_calculate_marginal_tax_rate_4000(self): | |
"""test case""" | |
measured = calculate_net_income(4000, datetime(2005, 5, 17), 0) | |
self.assertEqual(measured, 4000) | |
def test_calculate_marginal_tax_rate_under_4000(self): | |
"""test case""" | |
measured = calculate_net_income(3500, datetime(2005, 5, 17), 0) | |
self.assertEqual(measured, 3500) | |
def test_calculate_marginal_tax_rate_4000_7000_under20(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(2005, 5, 17), 0) | |
self.assertEqual(measured, 6350) | |
def test_calculate_marginal_tax_rate_4000_7000_no_age_reduction(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1957, 5, 17), 0) | |
self.assertEqual(measured, 6250) | |
def test_calculate_marginal_tax_rate_4000_7000_over65(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1955, 5, 17), 0) | |
self.assertEqual(measured, 6350) | |
def test_calculate_marginal_tax_rate_4000_7000_over75(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1942, 5, 17), 0) | |
self.assertEqual(measured, 6450) | |
def test_calculate_marginal_tax_rate_4000_7000_no_age_reduction_1_child(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1957, 5, 17), 1) | |
self.assertEqual(measured, 6300) | |
def test_calculate_marginal_tax_rate_4000_7000_no_age_reduction_2_children(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1957, 5, 17), 2) | |
self.assertEqual(measured, 6330) | |
def test_calculate_marginal_tax_rate_4000_7000_no_age_reduction_3_children(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1957, 5, 17), 3) | |
self.assertEqual(measured, 6350) | |
def test_calculate_marginal_tax_rate_4000_7000_no_age_reduction_6_children(self): | |
"""test case""" | |
measured = calculate_net_income(6500, datetime(1957, 5, 17), 6) | |
self.assertEqual(measured, 6350) | |
def test_calculate_marginal_tax_rate_income_is_negative(self): | |
"""test case""" | |
with self.assertRaises(ValueError): | |
calculate_net_income(-6500, datetime(1957, 5, 17), 6) | |
def test_calculate_marginal_tax_rate_number_of_children_is_negative(self): | |
"""test case""" | |
with self.assertRaises(ValueError): | |
calculate_net_income(6500, datetime(1957, 5, 17), -1) |
В этом примере у нас есть 12 тестовых случаев в нашем наборе, проверяющих несколько случаев в нашем коде.
Положительные тестовые случаи ожидают правильного расчета чистого дохода, а отрицательные тестовые случаи ожидают исключения из кода. .
Покрытие
Теперь пришло время посмотреть, насколько хорошо наш код покрыт юнит-тестами, с библиотекой покрытия это просто.
pip install covergage
Теперь мы можем запустить приложение покрытия, чтобы получить отчет о покрытии.
- выполнить тесты с покрытием:
> coverage run -m unittest tests\test_calculations.py
2. создать отчет о покрытии
> coverage report Name Stmts Miss Cover ------------------------------------------------ calculations.py 35 0 100% tests\test_calculations.py 40 0 100% ------------------------------------------------ TOTAL 75 0 100%
3. экспортировать в HTML
> coverage html
Потрясающий! 12 тестов, и мой код покрыт на 100%… ну, не совсем :(
Ограничение тестового покрытия
Покрытие тестами подсчитывает, сколько строк кода выполняется при выполнении теста.
При этом не учитываются различные пути выполнения, условные блоки и различные возможные комбинации.
Для этого мы используем мутационное тестирование!
При мутационном тестировании мы генерируем последовательность мутированной версии нашего исходного кода (он же — мутант).
Мы мутируем ее, изменяя операторы (например, '-' вместо '+') или изменяя операторы if ( например, добавив «не» к оператору if).
Затем мы запускаем наши наборы тестов на каждом из этих мутантов.
Это приводит к 3 возможным результатам для каждого мутанта:
- Сбой набора тестов — это означает, что набор тестов соответствует зараженным путям выполнения, это называется убитым мутантом.
- Тестовый набор пройден — это означает, что наш тестовый набор слаб для зараженных путей выполнения, это называется мутант выжил.
- Время ожидания набора тестов истекло — проблема с изменением кода в том, что это может привести к бесконечным циклам или бесконечным рекурсивным вызовам.
Очевидно, мы не можем знать, что находимся в бесконечном цикле.
Поэтому мы должны определить тайм-аут для завершения набора тестов.
В этом случае результат неясен.
Таким образом, в идеально закрытой кодовой базе мы ожидаем, что все мутанты будут убиты, и никто не выживет!
Знакомство с космическими лучами
Cosmic-Ray — отличный инструмент для выполнения мутационных тестов для приложений Python.
Шаг 1 — установите космический луч из репозитория PyPI:
pip install cosmic-ray
Шаг 2 — создайте файл конфигурации
эта часть интерактивна
cosmic-ray new-config config.ini
В результате файл config.ini выглядит примерно так:
[cosmic-ray] module-path = "calculations.py" timeout = 30.0 excluded-modules = [] test-command = "python -m unittest tests/test_calculations.py" [cosmic-ray.distributor] name = "local"
Шаг 3 — инициировать сеанс тестирования мутаций.
cosmic-ray init config.ini session.sqlite
Это создаст файл session.sqlite, в котором хранятся необработанные данные для тестирования мутаций.
Шаг 4 — создайте базовый уровень из базы данных.
cosmic-ray --verbosity=INFO baseline config.ini
Шаг 5 — выполните тестирование на мутацию.
cosmic-ray exec config.ini session.sqlite
Шаг 6 — создание отчета о тестировании мутаций
cr-html session.sqlite > report.html
Теперь мы можем открыть файл report.html в браузере.
Всего было создано и завершено 207 мутантов (без тайм-аутов), но из них 61, 29,47% выжили!
Это означает, что в моих тестовых наборах есть много уязвимых мест, а мой исходный код плохо защищен!
Давайте взглянем на список заданий и посмотрим, какие мутанты выжили.
Этот мутант заменил расчет маржинального налога на + вместо минуса, и набор тестов прошел!
Это вполне логично, поскольку мой набор тестов не включал значения выше 10000, где эта часть кода становится актуальной!
Ограничения тестирования мутаций
Основным ограничением тестирования мутаций является то, что оно интенсивно и требует много времени.
Cosmic Ray позволяет выполнять распределенное выполнение, чтобы сэкономить время и ускорить его, но, тем не менее, требует много ресурсов для своевременного запуска.
Другая проблема заключается в возможности бесконечных циклов, что приводит к неясному результату.
Заключение
Мутационное тестирование — отличный инструмент в поясе инструментов, который может дать вам важную информацию о качестве тестирования вашего кода.
Он предоставляет информацию, которую нельзя получить только путем расчета покрытия кода, но за счет времени и ресурсов.
В основном рекомендуется выполнять его нечасто, после того, как будет накоплено много изменений в кодовой базе и/или тестовом коде, чтобы получить представление о качестве теста.
Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.