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

Post on 14-Apr-2017

62 views 2 download

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

Цыганов Иван 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 = Trueprecision = 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 = Trueprecision = 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 jsonfrom 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

mutpyimport 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