Я расскажу о тестировании с точки зрения разработчика. Каждый разработчик рано или поздно приходит к выводу, что тесты необходимы. Следующий вопрос который он себе задает - а насколько хороши мои тесты. Я расскажу об инструментах и библиотеках, которые помогают оценить качество тестов в мире Python. Мы посмотрим как работает библиотека coverage.py, в каких ситуациях она бессильна и самое главное - почему. Узнаем что такое мутационное тестирование, как его можно применять в реальных проектах и как оно помогает оценить качество тестов. Увидим как работает библиотека Hypothesis и поймем в каких ситуациях она может оказаться нам полезной. В докладе будут внутренности Python-библиотек и объектов, много примеров и конечно же котики!
QA Fest 2019. Петр Тарасенко. QA Hackathon - The Cookbook 22
QA Fest 2017. Иван Цыганов. Не смешите мой coverage
1. Не смешите мой coverage!
Цыганов Иван
Positive Technologies
2. Обо мне
✤ Люблю OpenSource
✤ Не умею frontend
✤ Добровольно пишу тесты
3. ✤ Более 15 лет практического опыта на рынке ИБ
✤ Более 700 сотрудников в 9 странах
✤ Каждый год находим более 200 уязвимостей
нулевого дня
✤ Проводим более 200 аудитов безопасности в
крупнейших компаниях мира ежегодно
4. MaxPatrol
✤ Pentest. Тестирование на проникновение.
✤ Audit. Системные проверки.
✤ Compliance. Соответствие стандартам.
✤ Одна из крупнейших баз знаний в мире
Система контроля защищенности и соответствия
стандартам.
5. MaxPatrol
✤ Pentest. Тестирование на проникновение.
✤ Compliance. Соответствие стандартам.
✤ Одна из крупнейших баз знаний в мире
Система контроля защищенности и соответствия
стандартам.
✤ Audit. Системные проверки.
7. Зачем мы тестируем?
✤ Уверенность, что написанный код работает
✤ Ревью кода становится проще
✤ Гарантия, что ничего не сломалось при изменениях
8. Давайте писать тесты!
def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
result = int(digit, base=16)
else:
result = int(digit)
elif isinstance(digit, (int, float)):
result = digit
yield result
9. Плохой тест
assert list(normalize_digits(['1', '0x1'])) == [1, 1]
def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
result = int(digit, base=16)
else:
result = int(digit)
elif isinstance(digit, (int, float)):
result = digit
yield result
11. Неожиданные данные
>>> list(normalize_digits(['1', '2', '¯_(ツ)_/¯']))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in normalize_digits
ValueError: invalid literal for int() with base 10: '¯_(ツ)_/¯'
12. Неожиданные данные
>>> list(normalize_digits(['1', '2', '¯_(ツ)_/¯']))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in normalize_digits
ValueError: invalid literal for int() with base 10: '¯_(ツ)_/¯'
def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
try:
if digit.startswith('0x'):
result = int(digit, base=16)
else:
result = int(digit)
except ValueError:
continue
elif isinstance(digit, (int, float)):
result = digit
yield result
46. Выполненные строки
sys.settrace(tracefunc)
Set the system’s trace function, which allows you to implement a
Python source code debugger in Python.
Trace functions should have three arguments: frame, event, and
arg. frame is the current stack frame. event is a string: 'call',
'line', 'return', 'exception', 'c_call', 'c_return', or
'c_exception'. arg depends on the event type.
47. PyTracer «call» event
✤ Сохраняем данные предыдущего контекста
✤ Начинаем собирать данные нового контекста
✤ Учитываем особенности генераторов
60. Мутационное тестирование
✤ Берем тестируемый код
✤ Мутируем
✤ Тестируем мутантов нашими тестами
✤ Если тест не упал -> это плохой тест✤ Тест не упал -> плохой тест
63. Идея
def mul(a, b):
return a * b
def test_mul():
assert mul(2, 2) == 4
def mul(a, b):
return a ** b
def mul(a, b):
return a + b
64. Идея
def mul(a, b):
return a * b
def mul(a, b):
return a ** b
def mul(a, b):
return a + b
def test_mul():
assert mul(2, 2) == 4
assert mul(2, 3) == 6
65. mutpy
✤ Анализирует исходный код
✤ Модифицирует некоторые AST-ноды
✤ Запускает тесты
✤ Проверяет результат запуска тестов
82. Hypothesis. Генерация данных
>>> from hypothesis import strategies as st
>>> st.integers().example()
132598732931307445807900680032693714775
83. Hypothesis. Генерация данных
>>> from hypothesis import strategies as st
>>> st.integers().example()
132598732931307445807900680032693714775
>>> st.integers(min_value=0, max_value=100).example()
26
84. Hypothesis. Генерация данных
>>> from hypothesis import strategies as st
>>> st.integers().example()
132598732931307445807900680032693714775
>>> st.integers(min_value=0, max_value=100).example()
26
>>> st.text().example()
'U0007223dﶝU000d4ab2U000a477aU000c54e1# '
>>> st.text().example()
'傖'
97. def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
method = partial(int, base=16)
elif '.' in digit or 'e' in digit:
method = float
else:
method = int
elif isinstance(digit, (int, float)):
method = lambda x: x
else:
continue
try:
result = method(digit)
except ValueError:
continue
yield result
98. def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
method = partial(int, base=16)
elif '.' in digit or 'e' in digit:
method = float
else:
method = int
elif isinstance(digit, (int, float)):
method = lambda x: x
else:
continue
try:
result = method(digit)
except ValueError:
continue
yield result
def test_normalize():
assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10]
assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05]
99. def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
method = partial(int, base=16)
elif '.' in digit or 'e' in digit:
method = float
else:
method = int
elif isinstance(digit, (int, float)):
method = lambda x: x
else:
continue
try:
result = method(digit)
except ValueError:
continue
yield result
def test_normalize():
assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10]
assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05]
Name Stmts Miss Cover Missing
----------------------------------------------------
src.py 17 0 100.00%
100. def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
method = partial(int, base=16)
elif '.' in digit or 'e' in digit:
method = float
else:
method = int
elif isinstance(digit, (int, float)):
method = lambda x: x
else:
continue
try:
result = method(digit)
except ValueError:
continue
yield result
def test_normalize():
assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10]
assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05]
Name Stmts Miss Cover Missing
----------------------------------------------------
src.py 17 0 100.00%
Name Stmts Miss Branch BrPart Cover Missing
------------------------------------------------------------------
src.py 17 0 12 0 100.00%
101. def normalize_digits(digits_list):
for digit in digits_list:
if isinstance(digit, str):
if digit.startswith('0x'):
method = partial(int, base=16)
elif '.' in digit or 'e' in digit:
method = float
else:
method = int
elif isinstance(digit, (int, float)):
method = lambda x: x
else:
continue
try:
result = method(digit)
except ValueError:
continue
yield result
def test_normalize():
assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10]
assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05]
Name Stmts Miss Cover Missing
----------------------------------------------------
src.py 17 0 100.00%
Name Stmts Miss Branch BrPart Cover Missing
------------------------------------------------------------------
src.py 17 0 12 0 100.00%
[*] Mutation score [0.88045 s]: 100.0%
- all: 18
- killed: 18 (100.0%)
- survived: 0 (0.0%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)