SlideShare uma empresa Scribd logo
1 de 105
Baixar para ler offline
Цыганов Иван
Positive Technologies
Не доверяйте тестам!
Обо мне
✤ Спикер PyCon Russia 2016,
PiterPy#2 и PiterPy#3
✤ Люблю OpenSource
✤ Не умею frontend
✤ 15 лет практического опыта на рынке ИБ
✤ Более 650 сотрудников в 9 странах
✤ Каждый год находим более 200 уязвимостей
нулевого дня
✤ Проводим более 200 аудитов безопасности в
крупнейших компаниях мира ежегодно
MaxPatrol
✤ Тестирование на проникновение (Pentest)
✤ Системные проверки (Audit)
✤ Соответствие стандартам (Compliance)
✤ Одна из крупнейших баз знаний в мире
Система контроля защищенности и соответствия
стандартам.
✤ Тестирование на проникновение (Pentest)
✤ Системные проверки (Audit)
✤ Соответствие стандартам (Compliance)
✤ Одна из крупнейших баз знаний в мире
Система контроля защищенности и соответствия
стандартам.
✤ Системные проверки (Audit)
MaxPatrol
> 50 000 строк кода
Зачем тестировать?
✤ Уверенность, что написанный код работает
✤ Ревью кода становится проще
✤ Гарантия, что ничего не сломалось при
изменениях
есть тесты != код протестирован
Давайте писать тесты!
def get_total_price(cart_prices):
if len(cart_prices) == 0:
return
 
result = {'TotalPrice': sum(cart_prices)}
if len(cart_prices) >= 2:
result['Discount'] = result['TotalPrice'] * 0.25
 
return result['TotalPrice'] - result.get('Discount')
Плохой тест
def get_total_price(cart_prices):
if len(cart_prices) == 0:
return
 
result = {'TotalPrice': sum(cart_prices)}
if len(cart_prices) >= 2:
result['Discount'] = result['TotalPrice'] * 0.25
 
return result['TotalPrice'] - result.get('Discount')
def test_get_total_price():
assert get_total_price([90, 10]) == 75
Неожиданные данные
>>> balance = 1000
>>>
>>> goods = []
>>>
>>> balance -= get_total_price(goods)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -=: 'int' and 'NoneType'
>>>
есть тесты == есть тесты
Как сделать тесты лучше?
✤ Проверить покрытие кода тестами
✤ Попробовать мутационное тестирование
coverage.py
✤ Позволяет проверить покрытие кода тестами
✤ Есть плагин для pytest
coverage.py
✤ Позволяет проверить покрытие кода тестами
✤ Есть плагин для pytest
✤ В основном работает
coverage.ini
[report]

show_missing = True

precision = 2
py.test --cov-config=coverage.ini --cov=target test.py
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
def test_get_total_price():
assert get_total_price([90, 10]) == 75
Name Stmts Miss Cover Missing
--------------------------------------------
target.py 7 1 85.71% 2
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
def test_get_total_price():
assert get_total_price([90, 10]) == 75
Name Stmts Miss Cover Missing
--------------------------------------------
target.py 7 1 85.71% 2
2 if len(cart_prices) == 0:
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Cover Missing
--------------------------------------------
target.py 7 0 100.00%
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Cover Missing
--------------------------------------------
target.py 7 0 100.00%
>>> get_total_price([90])
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
>>> get_total_price([90])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in get_total_price
TypeError: unsupported operand type(s) for -: 'int' and
'NoneType'
>>>
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get('Discount')
coverage.ini
[report]

show_missing = True

precision = 2

[run]

branch = True
py.test --cov-config=coverage.ini --cov=target test.py
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 7 0 4 1 90.91% 6 ->9
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 7 0 4 1 90.91% 6 ->9
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
assert get_total_price([90]) == 90
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 7 0 4 0 100.00%
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 total_price = sum(cart_prices)
6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
7  
8 return total_price-get_discount(cart_prices, total_price)
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 total_price = sum(cart_prices)
6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
7  
8 return total_price-get_discount(cart_prices, total_price)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 6 1 4 1 80.00% 3, 2 ->3
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 total_price = sum(cart_prices)
6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
7  
8 return total_price-get_discount(cart_prices, total_price)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 6 0 4 0 100.00%
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 total_price = sum(cart_prices)
6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
7  
8 return total_price-get_discount(cart_prices, total_price)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 6 0 4 0 100.00%
6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
Как считать coverage?
Все строки
Реально выполненные
строки- Непокрытые
строки=
Все строки
Source
coverage.parser.PythonParser
Statements
coverage.parser.PythonParser
✤ Обходит все токены и отмечает «интересные»
факты
✤ Компилирует код. Обходит code-object и
сохраняет номера строк
Обход токенов
✤ Запоминает определения классов
✤ «Сворачивает» многострочные выражения
✤ Исключает комментарии
Обход байткода
✤ Полностью повторяет метод dis.findlinestarts
✤ Анализирует code_obj.co_lnotab
✤ Генерирует пару (номер байткода, номер строки)
Как считать coverage --branch?
Все переходы
Реально выполненные
переходы- Непокрытые
переходы=
Все переходы
Source
coverage.parser.AstArcAnalyzer
(from_line, to_line)
coverage.parser.PythonParser
coverage.parser.AstArcAnalyzer
✤ Обходит AST с корневой ноды
✤ Обрабатывает отдельно каждый тип нод отдельно
Обработка ноды
class While(stmt):
_fields = (
'test',
'body',
'orelse',
)
while i<10:
print(i)
i += 1
Обработка ноды
class While(stmt):
_fields = (
'test',
'body',
'orelse',
)
while i<10:
print(i)
i += 1
else:
print('All done')
Выполненные строки
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.
PyTracer «call» event
✤ Сохраняем данные предыдущего контекста
✤ Начинаем собирать данные нового контекста
✤ Учитываем особенности генераторов
PyTracer «line» event
✤ Запоминаем выполняемую строку
✤ Запоминаем переход между строками
PyTracer «return» event
✤ Отмечаем выход из контекста
✤ Помним о том, что yield это тоже return
Отчет
✤ Что выполнялось
✤ Что должно было выполниться
✤ Ругаемся
Зачем такие сложности?
1 for i in some_list:
2 if i == 'Hello':
3 print(i + ' World!')
4 elif i == 'Skip':
5 continue
6 else:
7 break
8 else:
9 print(r'¯_(ツ)_/¯')
Серебряная пуля?
Не совсем…
Что может пойти не так?
1 def make_dict(a,b,c):
2 return {
3 'a': a,
4 'b': b if a>1 else 0,
5 'c': [
6 i for i in range(c) if i<(a*10)
7 ]
6 }
Мутационное тестирование
✤ Берем тестируемый код
✤ Мутируем
✤ Тестируем мутантов нашими тестами
✤ Тест не упал -> плохой тест
Мутационное тестирование
✤ Берем тестируемый код
✤ Мутируем
✤ Тестируем мутантов нашими тестами
✤ Если тест не упал -> это плохой тест✤ Тест не упал -> плохой тест
Идея
def mul(a, b):
return a * b
def test_mul():
assert mul(2, 2) == 4
Идея
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
def test_mul():
assert mul(2, 2) == 4
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
def mul(a, b):
return a + b
def mul(a, b):
return a ** b
Tools
MutPy
✤ Проект заброшен
cosmic-ray
✤ Активно развивается
✤ Требует RabbitMQ
Реализация
Source
NodeTransformer
compile
run test
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] / 0.25
8  
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
9 return result['TotalPrice'] + result.get(‘Discount’, 0)
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
2 if (not len(cart_prices) == 0):
3 return 0
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
2 if len(cart_prices) == 1:
3 return 0
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
2 if len(cart_prices) == 0:
3 return 1
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
5 result = {'': sum(cart_prices)}
…
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
…
9 return result[‘some_key'] - result.get(‘Discount’, 0)
Мутации
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price(self):
self.assertEqual(get_total_price([90, 10]), 75)
self.assertEqual(get_total_price( []), 0)
self.assertEqual(get_total_price([90]), 90)
[*] Mutation score [0.50795 s]: 96.4%
- all: 28
- killed: 27 (96.4%)
- survived: 1 (3.6%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
1 def get_total_price(cart_prices):
2 if len(cart_prices) == 0:
3 return 0
4  
5 result = {'TotalPrice': sum(cart_prices)}
6 if len(cart_prices) >= 2:
7 result['Discount'] = result['TotalPrice'] * 0.25
8  
9 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price(self):
self.assertEqual(get_total_price([90, 10]), 75)
self.assertEqual(get_total_price( []), 0)
self.assertEqual(get_total_price([90]), 90)
[*] Mutation score [0.50795 s]: 96.4%
- all: 28
- killed: 27 (96.4%)
- survived: 1 (3.6%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
- survived: 1 (3.6%)
…
----------------------------------------------------------
1: def get_total_price(cart_prices):
2: if len(cart_prices) == 0:
~3: pass
4:
5: result = {'TotalPrice': sum(cart_prices)}
6: if len(cart_prices) >= 2:
7: result['Discount'] = result['TotalPrice'] * 0.25
8:
----------------------------------------------------------
[0.00968 s] survived
- [# 26] SDL target:5 :
…
[*] Mutation score [0.50795 s]: 96.4%
- all: 28
- killed: 27 (96.4%)
- survived: 1 (3.6%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
1 def get_total_price(cart_prices):  
2 result = {'TotalPrice': sum(cart_prices)}
3 if len(cart_prices) >= 2:
4 result['Discount'] = result['TotalPrice'] * 0.25
5  
6 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price(self):
self.assertEqual(get_total_price([90, 10]), 75)
self.assertEqual(get_total_price( []), 0)
self.assertEqual(get_total_price([90]), 90)
[*] Mutation score [0.44658 s]: 100.0%
- all: 23
- killed: 23 (100.0%)
- survived: 0 (0.0%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
Идея имеет право на жизнь и работает!
Но требует много ресурсов.
1 def get_total_price(cart_prices):  
2 result = {'TotalPrice': sum(cart_prices)}
3 if len(cart_prices) >= 2:
4 result['Discount'] = result['TotalPrice'] * 0.25
5  
6 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
assert get_total_price([90]) == 90
Name Stmts Miss Cover Missing
--------------------------------------------
target.py 5 0 100.00%
1 def get_total_price(cart_prices):  
2 result = {'TotalPrice': sum(cart_prices)}
3 if len(cart_prices) >= 2:
4 result['Discount'] = result['TotalPrice'] * 0.25
5  
6 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
assert get_total_price([90]) == 90
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 5 0 2 0 100.00%
1 def get_total_price(cart_prices):  
2 result = {'TotalPrice': sum(cart_prices)}
3 if len(cart_prices) >= 2:
4 result['Discount'] = result['TotalPrice'] * 0.25
5  
6 return result['TotalPrice'] - result.get(‘Discount’, 0)
def test_get_total_price():
assert get_total_price([90, 10]) == 75
assert get_total_price( []) == 0
assert get_total_price([90]) == 90
Name Stmts Miss Branch BrPart Cover Missing
----------------------------------------------------------
target.py 5 0 2 0 100.00%
Есть тесты != код протестирован
Есть тесты != код протестирован
Качество тестов важнее количества
Есть тесты != код протестирован
Качество тестов важнее количества
100% coverage - не повод расслабляться
Simple app
app = Flask(__name__)
 
@app.route('/get_total_discount', methods=['POST'])
def get_total_discount():
cart_prices = json.loads(request.form['cart_prices'])
 
result = {'TotalPrice': sum(cart_prices)}
if len(cart_prices) >= 2:
result['Discount'] = result['TotalPrice'] * 0.25
 
return jsonify(result['TotalPrice'] - result.get('Discount', 0))
flask_app.py
pip install pytest-flask
@pytest.fixture
def app():
from flask_app import app
return app
 
def test_get_total_discount(client):
get_total_discount = lambda prices: client.post(
'/get_total_discount',
data=dict(cart_prices=json.dumps(prices))
).json
 
assert get_total_discount([90, 10]) == 75
assert get_total_discount( []) == 0
assert get_total_discount([90]) == 90
test_flask_app.py
pip install pytest-flask
Name Stmts Miss Cover Missing
-----------------------------------------------
flask_app.py 9 0 100.00%
py.test --cov-config=coverage.ini 
--cov=flask_app 
test_flask_app.py
Name Stmts Miss Branch BrPart Cover Missing
-------------------------------------------------------------
flask_app.py 9 0 2 0 100.00%
py.test --cov-config=coverage_branch.ini 
--cov=flask_app 
test_flask_app.py
mutpy
class FlaskTestCase(unittest.TestCase):
def setUp(self):
self.app = flask_app.app.test_client()
 
def post(self, path, data):
return json.loads(self.app.post(path, data=data).data.decode('utf-8'))
 
def test_get_total_discount(self):
get_total_discount = lambda prices: self.post(
'/get_total_discount',
data=dict(cart_prices=json.dumps(prices))
)
self.assertEqual(get_total_discount([90, 10]), 75)
unittest_flask_app.py
mutpy
[*] Mutation score [0.39122 s]: 100.0%
- all: 27
- killed: 1 (3.7%)
- survived: 0 (0.0%)
- incompetent: 26 (96.3%)
- timeout: 0 (0.0%)
mut.py --target flask_app --unit-test unittest_flask_app
mutpy
[*] Mutation score [0.39122 s]: 100.0%
- all: 27
- killed: 1 (3.7%)
- survived: 0 (0.0%)
- incompetent: 26 (96.3%)
- timeout: 0 (0.0%)
mut.py --target flask_app --unit-test unittest_flask_app
mutpy
def _matching_loader_thinks_module_is_package(loader, mod_name):
#...
raise AttributeError(
('%s.is_package() method is missing but is required by Flask of '
'PEP 302 import hooks. If you do not use import hooks and '
'you encounter this error please file a bug against Flask.') %
loader.__class__.__name__)
mutpy
def _matching_loader_thinks_module_is_package(loader, mod_name):
#...
raise AttributeError(
('%s.is_package() method is missing but is required by Flask of '
'PEP 302 import hooks. If you do not use import hooks and '
'you encounter this error please file a bug against Flask.') %
loader.__class__.__name__)
class InjectImporter:
def __init__(self, module):
# ...
def find_module(self, fullname, path=None):
# ...
def load_module(self, fullname):
# ...
def install(self):
# ...
def uninstall(cls):
# ...
mutpy
class InjectImporter:
def __init__(self, module):
# ...
def find_module(self, fullname, path=None):
# ...
def load_module(self, fullname):
# ...
def install(self):
# ...
def uninstall(cls):
# …
def is_package(self, fullname):
# ...
mutpy
[*] Mutation score [1.14206 s]: 100.0%
- all: 27
- killed: 25 (92.6%)
- survived: 0 (0.0%)
- incompetent: 2 (7.4%)
- timeout: 0 (0.0%)
mut.py --target flask_app --unit-test unittest_flask_app
Simple app
import json
from django.http import HttpResponse
 
def index(request):
cart_prices = json.loads(request.POST['cart_prices'])
 
result = {'TotalPrice': sum(cart_prices)}
if len(cart_prices) >= 2:
result['Discount'] = result['TotalPrice'] * 0.25
 
return HttpResponse(result['TotalPrice'] - result.get('Discount', 0))
 
django_root/billing/views.py
pip install pytest-django
class TestCase1(TestCase):
def test_get_total_price(self):
get_total_price = lambda items: json.loads(
self.client.post(
'/billing/', data={'cart_prices': json.dumps(items)}
).content.decode('utf-8')
)
 
self.assertEqual(get_total_price([90, 10]), 75)
self.assertEqual(get_total_price( []), 0)
self.assertEqual(get_total_price([90]), 90)
django_root/billing/tests.py
pip install pytest-django
Name Stmts Miss Cover Missing
---------------------------------------------------
billing/views.py 8 0 100.00%
py.test --cov-config=coverage.ini 
--cov=billing.views 
billing/tests.py
Name Stmts Miss Branch BrPart Cover Missing
-----------------------------------------------------------------
billing/views.py 8 0 2 0 100.00%
py.test --cov-config=coverage_branch.ini 
--cov=billing.views 
billing/tests.py
mutpy
[*] Start mutation process:
- targets: billing.views
- tests: billing.tests
[*] Tests failed:
- error in setUpClass (billing.tests.TestCase1) -
django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES,
but settings are not configured. You must either define the environment
variable DJANGO_SETTINGS_MODULE or call settings.configure() before
accessing settings.
mut.py --target billing.views --unit-test billing.tests
mutpy
class Command(BaseCommand):
def handle(self, *args, **options):
operators_set = operators.standard_operators
if options['experimental_operators']:
operators_set |= operators.experimental_operators
 
controller = MutationController(
target_loader=ModulesLoader(options['target'], None),
test_loader=ModulesLoader(options['unit_test'], None),
views=[TextView(colored_output=False, show_mutants=True)],
mutant_generator=FirstOrderMutator(operators_set)
)
controller.run()
django_root/mutate_command/management/commands/mutate.py
mutpy
[*] Mutation score [1.07321 s]: 0.0%
- all: 22
- killed: 0 (0.0%)
- survived: 22 (100.0%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
python manage.py mutate 
--target billing.views
--unit-test billing.tests
mutpy
class RegexURLPattern(LocaleRegexProvider):
def __init__(self, regex, callback, default_args=None, name=None):
LocaleRegexProvider.__init__(self, regex)
self.callback = callback # the view
self.default_args = default_args or {}
self.name = name
django.urls.resolvers.RegexURLPattern
mutpy
import importlib
class Command(BaseCommand):
def hack_django_for_mutate(self):
def set_cb(self, value):
self._cb = value
 
def get_cb(self):
module = importlib.import_module(self._cb.__module__)
return module.__dict__.get(self._cb.__name__)
import django.urls.resolvers as r 
r.RegexURLPattern.callback = property(callback, set_cb)
 
def __init__(self, *args, **kwargs):
self.hack_django_for_mutate()
super().__init__(*args, **kwargs)
 
def add_arguments(self, parser):
# ...
mutpy
[*] Mutation score [1.48715 s]: 100.0%
- all: 22
- killed: 22 (100.0%)
- survived: 0 (0.0%)
- incompetent: 0 (0.0%)
- timeout: 0 (0.0%)
python manage.py mutate 
--target billing.views
--unit-test billing.tests
Спасибо за внимание! Вопросы?
mi.0-0.im
tsyganov-ivan.com
Links
✤ https://github.com/pytest-dev/pytest
✤ https://github.com/pytest-dev/pytest-flask
✤ https://github.com/pytest-dev/pytest-django
✤ https://bitbucket.org/ned/coveragepy
✤ https://github.com/pytest-dev/pytest-cov
✤ https://bitbucket.org/khalas/mutpy
✤ https://github.com/sixty-north/cosmic-ray

Mais conteúdo relacionado

Mais procurados

Php code for online quiz
Php code for online quizPhp code for online quiz
Php code for online quiz
hnyb1002
 
4. Метапрограмиране
4. Метапрограмиране4. Метапрограмиране
4. Метапрограмиране
Stefan Kanev
 

Mais procurados (19)

Mocking Dependencies in PHPUnit
Mocking Dependencies in PHPUnitMocking Dependencies in PHPUnit
Mocking Dependencies in PHPUnit
 
Programação funcional em Python
Programação funcional em PythonProgramação funcional em Python
Programação funcional em Python
 
Php code for online quiz
Php code for online quizPhp code for online quiz
Php code for online quiz
 
K12
K12K12
K12
 
Ruby things
Ruby thingsRuby things
Ruby things
 
Form tour person1
Form tour person1Form tour person1
Form tour person1
 
What Have The Properties Ever Done For Us
What Have The Properties Ever Done For UsWhat Have The Properties Ever Done For Us
What Have The Properties Ever Done For Us
 
Advanced php testing in action
Advanced php testing in actionAdvanced php testing in action
Advanced php testing in action
 
The secret unit testing tools no one has ever told you about
The secret unit testing tools no one has ever told you aboutThe secret unit testing tools no one has ever told you about
The secret unit testing tools no one has ever told you about
 
Neo4 J
Neo4 J Neo4 J
Neo4 J
 
Refactor like a boss
Refactor like a bossRefactor like a boss
Refactor like a boss
 
code for quiz in my sql
code for quiz  in my sql code for quiz  in my sql
code for quiz in my sql
 
inception.docx
inception.docxinception.docx
inception.docx
 
From typing the test to testing the type
From typing the test to testing the typeFrom typing the test to testing the type
From typing the test to testing the type
 
Five
FiveFive
Five
 
A Tour to MySQL Commands
A Tour to MySQL CommandsA Tour to MySQL Commands
A Tour to MySQL Commands
 
4. Метапрограмиране
4. Метапрограмиране4. Метапрограмиране
4. Метапрограмиране
 
Refactoring Ruby Code
Refactoring Ruby CodeRefactoring Ruby Code
Refactoring Ruby Code
 
令和から本気出す
令和から本気出す令和から本気出す
令和から本気出す
 

Semelhante a PyCon Siberia 2016. Не доверяйте тестам!

C-Sharp Arithmatic Expression Calculator
C-Sharp Arithmatic Expression CalculatorC-Sharp Arithmatic Expression Calculator
C-Sharp Arithmatic Expression Calculator
Neeraj Kaushik
 
Trading with opensource tools, two years later
Trading with opensource tools, two years laterTrading with opensource tools, two years later
Trading with opensource tools, two years later
clkao
 
Ruby Language - A quick tour
Ruby Language - A quick tourRuby Language - A quick tour
Ruby Language - A quick tour
aztack
 

Semelhante a PyCon Siberia 2016. Не доверяйте тестам! (20)

Testing in those hard to reach places
Testing in those hard to reach placesTesting in those hard to reach places
Testing in those hard to reach places
 
Parametrized testing, v2
Parametrized testing, v2Parametrized testing, v2
Parametrized testing, v2
 
The Ring programming language version 1.7 book - Part 10 of 196
The Ring programming language version 1.7 book - Part 10 of 196The Ring programming language version 1.7 book - Part 10 of 196
The Ring programming language version 1.7 book - Part 10 of 196
 
The Ring programming language version 1.6 book - Part 9 of 189
The Ring programming language version 1.6 book - Part 9 of 189The Ring programming language version 1.6 book - Part 9 of 189
The Ring programming language version 1.6 book - Part 9 of 189
 
집단지성 프로그래밍 08-가격모델링
집단지성 프로그래밍 08-가격모델링집단지성 프로그래밍 08-가격모델링
집단지성 프로그래밍 08-가격모델링
 
C-Sharp Arithmatic Expression Calculator
C-Sharp Arithmatic Expression CalculatorC-Sharp Arithmatic Expression Calculator
C-Sharp Arithmatic Expression Calculator
 
Performance
PerformancePerformance
Performance
 
Home Work; Chapter 9; Inventory Policy Decisions
Home Work; Chapter 9; Inventory Policy DecisionsHome Work; Chapter 9; Inventory Policy Decisions
Home Work; Chapter 9; Inventory Policy Decisions
 
lecture7.ppt
lecture7.pptlecture7.ppt
lecture7.ppt
 
Patterns for slick database applications
Patterns for slick database applicationsPatterns for slick database applications
Patterns for slick database applications
 
Optimization and Mathematical Programming in R and ROI - R Optimization Infra...
Optimization and Mathematical Programming in R and ROI - R Optimization Infra...Optimization and Mathematical Programming in R and ROI - R Optimization Infra...
Optimization and Mathematical Programming in R and ROI - R Optimization Infra...
 
Trading with opensource tools, two years later
Trading with opensource tools, two years laterTrading with opensource tools, two years later
Trading with opensource tools, two years later
 
Power shell voor developers
Power shell voor developersPower shell voor developers
Power shell voor developers
 
Naive application of Machine Learning to Software Development
Naive application of Machine Learning to Software DevelopmentNaive application of Machine Learning to Software Development
Naive application of Machine Learning to Software Development
 
R console
R consoleR console
R console
 
Symfony (Unit, Functional) Testing.
Symfony (Unit, Functional) Testing.Symfony (Unit, Functional) Testing.
Symfony (Unit, Functional) Testing.
 
Tt subtemplates-caching
Tt subtemplates-cachingTt subtemplates-caching
Tt subtemplates-caching
 
Quality Python Homework Help
Quality Python Homework HelpQuality Python Homework Help
Quality Python Homework Help
 
Ruby Language - A quick tour
Ruby Language - A quick tourRuby Language - A quick tour
Ruby Language - A quick tour
 
Php my sql - functions - arrays - tutorial - programmerblog.net
Php my sql - functions - arrays - tutorial - programmerblog.netPhp my sql - functions - arrays - tutorial - programmerblog.net
Php my sql - functions - arrays - tutorial - programmerblog.net
 

Último

+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
Health
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
VishalKumarJha10
 

Último (20)

+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial Goals
 
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
The Guide to Integrating Generative AI into Unified Continuous Testing Platfo...
 
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerHow To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
 
The Real-World Challenges of Medical Device Cybersecurity- Mitigating Vulnera...
The Real-World Challenges of Medical Device Cybersecurity- Mitigating Vulnera...The Real-World Challenges of Medical Device Cybersecurity- Mitigating Vulnera...
The Real-World Challenges of Medical Device Cybersecurity- Mitigating Vulnera...
 
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
 
Define the academic and professional writing..pdf
Define the academic and professional writing..pdfDefine the academic and professional writing..pdf
Define the academic and professional writing..pdf
 
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time ApplicationsUnveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
 
Pharm-D Biostatistics and Research methodology
Pharm-D Biostatistics and Research methodologyPharm-D Biostatistics and Research methodology
Pharm-D Biostatistics and Research methodology
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...
 
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
Direct Style Effect Systems -The Print[A] Example- A Comprehension AidDirect Style Effect Systems -The Print[A] Example- A Comprehension Aid
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
 
Exploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdfExploring the Best Video Editing App.pdf
Exploring the Best Video Editing App.pdf
 
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdfAzure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
Azure_Native_Qumulo_High_Performance_Compute_Benchmarks.pdf
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
 
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
%in Bahrain+277-882-255-28 abortion pills for sale in Bahrain
 
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdfintroduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
introduction-to-automotive Andoid os-csimmonds-ndctechtown-2021.pdf
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
 

PyCon Siberia 2016. Не доверяйте тестам!

  • 1. Цыганов Иван Positive Technologies Не доверяйте тестам!
  • 2. Обо мне ✤ Спикер PyCon Russia 2016, PiterPy#2 и PiterPy#3 ✤ Люблю OpenSource ✤ Не умею frontend
  • 3. ✤ 15 лет практического опыта на рынке ИБ ✤ Более 650 сотрудников в 9 странах ✤ Каждый год находим более 200 уязвимостей нулевого дня ✤ Проводим более 200 аудитов безопасности в крупнейших компаниях мира ежегодно
  • 4.
  • 5. MaxPatrol ✤ Тестирование на проникновение (Pentest) ✤ Системные проверки (Audit) ✤ Соответствие стандартам (Compliance) ✤ Одна из крупнейших баз знаний в мире Система контроля защищенности и соответствия стандартам.
  • 6. ✤ Тестирование на проникновение (Pentest) ✤ Системные проверки (Audit) ✤ Соответствие стандартам (Compliance) ✤ Одна из крупнейших баз знаний в мире Система контроля защищенности и соответствия стандартам. ✤ Системные проверки (Audit) MaxPatrol
  • 7. > 50 000 строк кода
  • 8. Зачем тестировать? ✤ Уверенность, что написанный код работает ✤ Ревью кода становится проще ✤ Гарантия, что ничего не сломалось при изменениях
  • 9. есть тесты != код протестирован
  • 10. Давайте писать тесты! def get_total_price(cart_prices): if len(cart_prices) == 0: return   result = {'TotalPrice': sum(cart_prices)} if len(cart_prices) >= 2: result['Discount'] = result['TotalPrice'] * 0.25   return result['TotalPrice'] - result.get('Discount')
  • 11. Плохой тест def get_total_price(cart_prices): if len(cart_prices) == 0: return   result = {'TotalPrice': sum(cart_prices)} if len(cart_prices) >= 2: result['Discount'] = result['TotalPrice'] * 0.25   return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75
  • 12. Неожиданные данные >>> balance = 1000 >>> >>> goods = [] >>> >>> balance -= get_total_price(goods) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for -=: 'int' and 'NoneType' >>>
  • 13. есть тесты == есть тесты
  • 14. Как сделать тесты лучше? ✤ Проверить покрытие кода тестами ✤ Попробовать мутационное тестирование
  • 15. coverage.py ✤ Позволяет проверить покрытие кода тестами ✤ Есть плагин для pytest
  • 16. coverage.py ✤ Позволяет проверить покрытие кода тестами ✤ Есть плагин для pytest ✤ В основном работает
  • 17. coverage.ini [report]
 show_missing = True
 precision = 2 py.test --cov-config=coverage.ini --cov=target test.py
  • 18. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75 Name Stmts Miss Cover Missing -------------------------------------------- target.py 7 1 85.71% 2
  • 19. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75 Name Stmts Miss Cover Missing -------------------------------------------- target.py 7 1 85.71% 2 2 if len(cart_prices) == 0:
  • 20. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Cover Missing -------------------------------------------- target.py 7 0 100.00%
  • 21. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Cover Missing -------------------------------------------- target.py 7 0 100.00%
  • 22. >>> get_total_price([90]) 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount')
  • 23. >>> get_total_price([90]) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 9, in get_total_price TypeError: unsupported operand type(s) for -: 'int' and 'NoneType' >>> 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get('Discount')
  • 24.
  • 25. coverage.ini [report]
 show_missing = True
 precision = 2
 [run]
 branch = True py.test --cov-config=coverage.ini --cov=target test.py
  • 26. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 7 0 4 1 90.91% 6 ->9
  • 27. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 7 0 4 1 90.91% 6 ->9
  • 28. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 assert get_total_price([90]) == 90 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 7 0 4 0 100.00%
  • 29.
  • 30. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 total_price = sum(cart_prices) 6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25 7   8 return total_price-get_discount(cart_prices, total_price) 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0)
  • 31. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 total_price = sum(cart_prices) 6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25 7   8 return total_price-get_discount(cart_prices, total_price) def test_get_total_price(): assert get_total_price([90, 10]) == 75 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 6 1 4 1 80.00% 3, 2 ->3
  • 32. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 total_price = sum(cart_prices) 6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25 7   8 return total_price-get_discount(cart_prices, total_price) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 6 0 4 0 100.00%
  • 33. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 total_price = sum(cart_prices) 6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25 7   8 return total_price-get_discount(cart_prices, total_price) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 6 0 4 0 100.00% 6 get_discount = lambda items, price: len(items) >= 2 and price * 0.25
  • 34.
  • 35. Как считать coverage? Все строки Реально выполненные строки- Непокрытые строки=
  • 37. coverage.parser.PythonParser ✤ Обходит все токены и отмечает «интересные» факты ✤ Компилирует код. Обходит code-object и сохраняет номера строк
  • 38. Обход токенов ✤ Запоминает определения классов ✤ «Сворачивает» многострочные выражения ✤ Исключает комментарии
  • 39. Обход байткода ✤ Полностью повторяет метод dis.findlinestarts ✤ Анализирует code_obj.co_lnotab ✤ Генерирует пару (номер байткода, номер строки)
  • 40. Как считать coverage --branch? Все переходы Реально выполненные переходы- Непокрытые переходы=
  • 42. coverage.parser.AstArcAnalyzer ✤ Обходит AST с корневой ноды ✤ Обрабатывает отдельно каждый тип нод отдельно
  • 43. Обработка ноды class While(stmt): _fields = ( 'test', 'body', 'orelse', ) while i<10: print(i) i += 1
  • 44. Обработка ноды class While(stmt): _fields = ( 'test', 'body', 'orelse', ) while i<10: print(i) i += 1 else: print('All done')
  • 45. Выполненные строки 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.
  • 46. PyTracer «call» event ✤ Сохраняем данные предыдущего контекста ✤ Начинаем собирать данные нового контекста ✤ Учитываем особенности генераторов
  • 47. PyTracer «line» event ✤ Запоминаем выполняемую строку ✤ Запоминаем переход между строками
  • 48. PyTracer «return» event ✤ Отмечаем выход из контекста ✤ Помним о том, что yield это тоже return
  • 49. Отчет ✤ Что выполнялось ✤ Что должно было выполниться ✤ Ругаемся
  • 50. Зачем такие сложности? 1 for i in some_list: 2 if i == 'Hello': 3 print(i + ' World!') 4 elif i == 'Skip': 5 continue 6 else: 7 break 8 else: 9 print(r'¯_(ツ)_/¯')
  • 53. Что может пойти не так? 1 def make_dict(a,b,c): 2 return { 3 'a': a, 4 'b': b if a>1 else 0, 5 'c': [ 6 i for i in range(c) if i<(a*10) 7 ] 6 }
  • 54.
  • 55. Мутационное тестирование ✤ Берем тестируемый код ✤ Мутируем ✤ Тестируем мутантов нашими тестами ✤ Тест не упал -> плохой тест
  • 56. Мутационное тестирование ✤ Берем тестируемый код ✤ Мутируем ✤ Тестируем мутантов нашими тестами ✤ Если тест не упал -> это плохой тест✤ Тест не упал -> плохой тест
  • 57. Идея def mul(a, b): return a * b def test_mul(): assert mul(2, 2) == 4
  • 58. Идея def mul(a, b): return a * b def test_mul(): assert mul(2, 2) == 4 def mul(a, b): return a ** b
  • 59. Идея 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
  • 60. Идея def mul(a, b): return a * b def test_mul(): assert mul(2, 2) == 4 assert mul(2, 3) == 6 def mul(a, b): return a + b def mul(a, b): return a ** b
  • 61. Tools MutPy ✤ Проект заброшен cosmic-ray ✤ Активно развивается ✤ Требует RabbitMQ
  • 63. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] / 0.25 8   …
  • 64. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 9 return result['TotalPrice'] + result.get(‘Discount’, 0) …
  • 65. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 2 if (not len(cart_prices) == 0): 3 return 0 …
  • 66. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 2 if len(cart_prices) == 1: 3 return 0 …
  • 67. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 2 if len(cart_prices) == 0: 3 return 1 …
  • 68. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 5 result = {'': sum(cart_prices)} …
  • 69. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) … 9 return result[‘some_key'] - result.get(‘Discount’, 0)
  • 70. Мутации 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0)
  • 71. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(self): self.assertEqual(get_total_price([90, 10]), 75) self.assertEqual(get_total_price( []), 0) self.assertEqual(get_total_price([90]), 90) [*] Mutation score [0.50795 s]: 96.4% - all: 28 - killed: 27 (96.4%) - survived: 1 (3.6%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%)
  • 72. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return 0 4   5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8   9 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(self): self.assertEqual(get_total_price([90, 10]), 75) self.assertEqual(get_total_price( []), 0) self.assertEqual(get_total_price([90]), 90) [*] Mutation score [0.50795 s]: 96.4% - all: 28 - killed: 27 (96.4%) - survived: 1 (3.6%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) - survived: 1 (3.6%)
  • 73. … ---------------------------------------------------------- 1: def get_total_price(cart_prices): 2: if len(cart_prices) == 0: ~3: pass 4: 5: result = {'TotalPrice': sum(cart_prices)} 6: if len(cart_prices) >= 2: 7: result['Discount'] = result['TotalPrice'] * 0.25 8: ---------------------------------------------------------- [0.00968 s] survived - [# 26] SDL target:5 : … [*] Mutation score [0.50795 s]: 96.4% - all: 28 - killed: 27 (96.4%) - survived: 1 (3.6%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%)
  • 74. 1 def get_total_price(cart_prices):   2 result = {'TotalPrice': sum(cart_prices)} 3 if len(cart_prices) >= 2: 4 result['Discount'] = result['TotalPrice'] * 0.25 5   6 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(self): self.assertEqual(get_total_price([90, 10]), 75) self.assertEqual(get_total_price( []), 0) self.assertEqual(get_total_price([90]), 90) [*] Mutation score [0.44658 s]: 100.0% - all: 23 - killed: 23 (100.0%) - survived: 0 (0.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%)
  • 75. Идея имеет право на жизнь и работает! Но требует много ресурсов.
  • 76. 1 def get_total_price(cart_prices):   2 result = {'TotalPrice': sum(cart_prices)} 3 if len(cart_prices) >= 2: 4 result['Discount'] = result['TotalPrice'] * 0.25 5   6 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 assert get_total_price([90]) == 90 Name Stmts Miss Cover Missing -------------------------------------------- target.py 5 0 100.00%
  • 77. 1 def get_total_price(cart_prices):   2 result = {'TotalPrice': sum(cart_prices)} 3 if len(cart_prices) >= 2: 4 result['Discount'] = result['TotalPrice'] * 0.25 5   6 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 assert get_total_price([90]) == 90 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 5 0 2 0 100.00%
  • 78. 1 def get_total_price(cart_prices):   2 result = {'TotalPrice': sum(cart_prices)} 3 if len(cart_prices) >= 2: 4 result['Discount'] = result['TotalPrice'] * 0.25 5   6 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 assert get_total_price([90]) == 90 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 5 0 2 0 100.00%
  • 79. Есть тесты != код протестирован
  • 80. Есть тесты != код протестирован Качество тестов важнее количества
  • 81. Есть тесты != код протестирован Качество тестов важнее количества 100% coverage - не повод расслабляться
  • 82.
  • 83.
  • 84. Simple app app = Flask(__name__)   @app.route('/get_total_discount', methods=['POST']) def get_total_discount(): cart_prices = json.loads(request.form['cart_prices'])   result = {'TotalPrice': sum(cart_prices)} if len(cart_prices) >= 2: result['Discount'] = result['TotalPrice'] * 0.25   return jsonify(result['TotalPrice'] - result.get('Discount', 0)) flask_app.py
  • 85. pip install pytest-flask @pytest.fixture def app(): from flask_app import app return app   def test_get_total_discount(client): get_total_discount = lambda prices: client.post( '/get_total_discount', data=dict(cart_prices=json.dumps(prices)) ).json   assert get_total_discount([90, 10]) == 75 assert get_total_discount( []) == 0 assert get_total_discount([90]) == 90 test_flask_app.py
  • 86. pip install pytest-flask Name Stmts Miss Cover Missing ----------------------------------------------- flask_app.py 9 0 100.00% py.test --cov-config=coverage.ini --cov=flask_app test_flask_app.py Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------- flask_app.py 9 0 2 0 100.00% py.test --cov-config=coverage_branch.ini --cov=flask_app test_flask_app.py
  • 87. mutpy class FlaskTestCase(unittest.TestCase): def setUp(self): self.app = flask_app.app.test_client()   def post(self, path, data): return json.loads(self.app.post(path, data=data).data.decode('utf-8'))   def test_get_total_discount(self): get_total_discount = lambda prices: self.post( '/get_total_discount', data=dict(cart_prices=json.dumps(prices)) ) self.assertEqual(get_total_discount([90, 10]), 75) unittest_flask_app.py
  • 88. mutpy [*] Mutation score [0.39122 s]: 100.0% - all: 27 - killed: 1 (3.7%) - survived: 0 (0.0%) - incompetent: 26 (96.3%) - timeout: 0 (0.0%) mut.py --target flask_app --unit-test unittest_flask_app
  • 89. mutpy [*] Mutation score [0.39122 s]: 100.0% - all: 27 - killed: 1 (3.7%) - survived: 0 (0.0%) - incompetent: 26 (96.3%) - timeout: 0 (0.0%) mut.py --target flask_app --unit-test unittest_flask_app
  • 90. mutpy def _matching_loader_thinks_module_is_package(loader, mod_name): #... raise AttributeError( ('%s.is_package() method is missing but is required by Flask of ' 'PEP 302 import hooks. If you do not use import hooks and ' 'you encounter this error please file a bug against Flask.') % loader.__class__.__name__)
  • 91. mutpy def _matching_loader_thinks_module_is_package(loader, mod_name): #... raise AttributeError( ('%s.is_package() method is missing but is required by Flask of ' 'PEP 302 import hooks. If you do not use import hooks and ' 'you encounter this error please file a bug against Flask.') % loader.__class__.__name__) class InjectImporter: def __init__(self, module): # ... def find_module(self, fullname, path=None): # ... def load_module(self, fullname): # ... def install(self): # ... def uninstall(cls): # ...
  • 92. mutpy class InjectImporter: def __init__(self, module): # ... def find_module(self, fullname, path=None): # ... def load_module(self, fullname): # ... def install(self): # ... def uninstall(cls): # … def is_package(self, fullname): # ...
  • 93. mutpy [*] Mutation score [1.14206 s]: 100.0% - all: 27 - killed: 25 (92.6%) - survived: 0 (0.0%) - incompetent: 2 (7.4%) - timeout: 0 (0.0%) mut.py --target flask_app --unit-test unittest_flask_app
  • 94.
  • 95. Simple app import json from django.http import HttpResponse   def index(request): cart_prices = json.loads(request.POST['cart_prices'])   result = {'TotalPrice': sum(cart_prices)} if len(cart_prices) >= 2: result['Discount'] = result['TotalPrice'] * 0.25   return HttpResponse(result['TotalPrice'] - result.get('Discount', 0))   django_root/billing/views.py
  • 96. pip install pytest-django class TestCase1(TestCase): def test_get_total_price(self): get_total_price = lambda items: json.loads( self.client.post( '/billing/', data={'cart_prices': json.dumps(items)} ).content.decode('utf-8') )   self.assertEqual(get_total_price([90, 10]), 75) self.assertEqual(get_total_price( []), 0) self.assertEqual(get_total_price([90]), 90) django_root/billing/tests.py
  • 97. pip install pytest-django Name Stmts Miss Cover Missing --------------------------------------------------- billing/views.py 8 0 100.00% py.test --cov-config=coverage.ini --cov=billing.views billing/tests.py Name Stmts Miss Branch BrPart Cover Missing ----------------------------------------------------------------- billing/views.py 8 0 2 0 100.00% py.test --cov-config=coverage_branch.ini --cov=billing.views billing/tests.py
  • 98. mutpy [*] Start mutation process: - targets: billing.views - tests: billing.tests [*] Tests failed: - error in setUpClass (billing.tests.TestCase1) - django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings. mut.py --target billing.views --unit-test billing.tests
  • 99. mutpy class Command(BaseCommand): def handle(self, *args, **options): operators_set = operators.standard_operators if options['experimental_operators']: operators_set |= operators.experimental_operators   controller = MutationController( target_loader=ModulesLoader(options['target'], None), test_loader=ModulesLoader(options['unit_test'], None), views=[TextView(colored_output=False, show_mutants=True)], mutant_generator=FirstOrderMutator(operators_set) ) controller.run() django_root/mutate_command/management/commands/mutate.py
  • 100. mutpy [*] Mutation score [1.07321 s]: 0.0% - all: 22 - killed: 0 (0.0%) - survived: 22 (100.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) python manage.py mutate --target billing.views --unit-test billing.tests
  • 101. mutpy class RegexURLPattern(LocaleRegexProvider): def __init__(self, regex, callback, default_args=None, name=None): LocaleRegexProvider.__init__(self, regex) self.callback = callback # the view self.default_args = default_args or {} self.name = name django.urls.resolvers.RegexURLPattern
  • 102. mutpy import importlib class Command(BaseCommand): def hack_django_for_mutate(self): def set_cb(self, value): self._cb = value   def get_cb(self): module = importlib.import_module(self._cb.__module__) return module.__dict__.get(self._cb.__name__) import django.urls.resolvers as r  r.RegexURLPattern.callback = property(callback, set_cb)   def __init__(self, *args, **kwargs): self.hack_django_for_mutate() super().__init__(*args, **kwargs)   def add_arguments(self, parser): # ...
  • 103. mutpy [*] Mutation score [1.48715 s]: 100.0% - all: 22 - killed: 22 (100.0%) - survived: 0 (0.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) python manage.py mutate --target billing.views --unit-test billing.tests
  • 104. Спасибо за внимание! Вопросы? mi.0-0.im tsyganov-ivan.com
  • 105. Links ✤ https://github.com/pytest-dev/pytest ✤ https://github.com/pytest-dev/pytest-flask ✤ https://github.com/pytest-dev/pytest-django ✤ https://bitbucket.org/ned/coveragepy ✤ https://github.com/pytest-dev/pytest-cov ✤ https://bitbucket.org/khalas/mutpy ✤ https://github.com/sixty-north/cosmic-ray