«QuickCheck в Python: проверка гипотез и поиск ошибок»,...

  • View
    8.489

  • Download
    0

  • Category

    Software

Preview:

Citation preview

QuickCheck в PythonQuickCheck в PythonПроверка гипотез и поиск ошибокПроверка гипотез и поиск ошибок

Шорин Александр / @kxepal

О чем пойдет речь:О чем пойдет речь:Что не так с нашими тестамиЧто такое QuickCheck......и как его неправильно портироватьЗнакомство с HypothesisВремя офигенных историй

Что не так с нашими тестами?Что не так с нашими тестами?

Простейший примерПростейший примерdef qsort(list_):

if len(list_) <= 1:return list_

head = qsort([x for x in list_[1:] if x < list_[0]]) tail = qsort([x for x in list_[1:] if x > list_[0]])

return head + [list_[0]] + tail

(да, в жизни все сложнее)(да, в жизни все сложнее)

ТестируемТестируемdef test_empty():

assert qsort([]) == []

def test_sort():assert qsort([3, 2, 1]) == [1, 2, 3]

def test_sorted():assert qsort([1, 2, 3]) == [1, 2, 3]

Шаблонная копипастаШаблонная копипаста

Мы можем решить проблему копипасты...Мы можем решить проблему копипасты...

@pytest.mark.parametrize(('value', 'result'), [ ([], []), ([1, 2, 3], [1, 2, 3]), ([3, 2, 1], [1, 2, 3]),])def test_qsort(value, result):

assert qsort(value) == result

...хотя может показаться, что нет...хотя может показаться, что нет

@pytest.mark.parametrize(('value', 'result'), [ ([], []), ([1, 2, 3], [1, 2, 3]), ([3, 2, 1], [1, 2, 3]), (['a', 'c', 'b'], ['a', 'b', 'c']), (['a', 'A'], ['A', 'a']), ([[0], [3], [1, 2, 3]], [[0], [1, 2, 3], [3]]), ...])def test_qsort(value, result):

assert qsort(value) == result

Можем ли мы проверить все допустимые случаи?Можем ли мы проверить все допустимые случаи?+----------------------------------------------------------------------+| ВЕЛИКОЕ ВСЁ || || || || || || || || || || +---------------------+| | Приемлемые данные || | Х |+------------------------------------------------+-------------------^-+ | Наши тесты -------+

Ошибку в коде мы так и неОшибку в коде мы так и ненашлинашли

def test_qsort():> assert qsort([1, 1, 1]) == [1, 1, 1]E assert [1] == [1, 1, 1]E Right contains more items, first extra item: 1E Full diff:E - [1]E + [1, 1, 1]

Bug Driven DevelopmentBug Driven DevelopmentBug. Test. Fix. Repeat.Bug. Test. Fix. Repeat.

РезюмируемРезюмируемМы не тестируем наш код, мы закрепляем егоповедениеМы не способны описать все пограничныеслучаиБаги всегда впереди нас

QuickCheckQuickCheckИзначально написан на HaskellОсновывается на научно-исследовательскихработахПозиционируется как библиотека дляавтоматического тестирование функций наоснове спецификаций и свойств данныхПортирован на Scala, Erlang, Clojure, JavaScript...Продвигается компанией Quviqhttp://www.cse.chalmers.se/~rjmh/QuickCheck/

Как оно работает (примерно)Как оно работает (примерно) passed +-----------------------+ v |+-----------+ sample +------+ failed +----------+ +--------+| Generator | --------> | Test | --------> | Shrinker | --> | Report |+-----------+ +------+ +----------+ +--------+ | ^ sample | ^ | +------------------+ | | | +-----------------------------------------------------------+ success

Выглядит просто?Выглядит просто?

Нельзя просто взять и написатьНельзя просто взять и написатьправильный QuickCheckправильный QuickCheck

https://github.com/agrif/pyquickcheckhttps://github.com/Cue/qchttps://github.com/dbravender/qchttps://github.com/futoase/PyQCheckhttps://github.com/JesseBuesking/pythoncheckhttps://github.com/markchadwick/paycheckhttps://github.com/msoedov/quick.pyhttps://github.com/npryce/python-factcheckhttps://github.com/Xion/pyqcyhttps://github.com/zombiecalypse/qcchttps://pypi.python.org/pypi/pytest-quickcheck...

Типичные ошибкиТипичные ошибкиСлепое копирование Haskell реализацииИспользование типов в качестве генераторовОтсутствующий или же "глупый" shrinkingЗаброшенные или же в зачаточном состоянии

Hypothesis [haɪˈpɒθɪsɪs]Hypothesis [haɪˈpɒθɪsɪs]Автор: David R. MacIver

https://github.com/DRMacIver/hypothesis

Что умеетЧто умеетГенерация всех основных типов данныхРекурсивные типыState Machine, N-ary деревьяПозволяет создавать свои стратегии безпогружения в деталиИнтегрирован с Fake Factory, Django, pytestСледит за качеством тестовЗапоминает найденные багиХорошо настраивается

http://hypothesis.readthedocs.org/en/master/data.htmlhttp://hypothesis.readthedocs.org/en/master/data.html

Генерация данныхГенерация данныхfrom hypothesis import strategies as st

def nulls(): return st.none()def booleans(): return st.booleans()def numbers(): return st.integers() | st.floats()def strings(): return st.text()def arrays(elements): return st.lists(elements)def objects(elements): return st.dictionaries(strings(), elements)def values(): simple_values = nulls() | booleans() | numbers() | strings()

return (simple_values | st.recursive(simple_values,

lambda children: arrays(children) | objects(children)))

Генерация данныхГенерация данных>>> doc = json_st.objects(json_st.values())>>> doc.example(){'G G\u202fn G𝞠\u202f_n( n(nn_ n': 9.943339378805967e-309}>>> doc.example(){'': None, '\x85': '', '\U00014481': None,'\u3000': -2.45410883359415e-309, ' \x85': 1.5564453946197205e-308,'I': ' ','\u3000\u2029': -9.05230966734913e-309, '\U00014481\U00014481 ⁱ': None,'Nj': -1.80149788818e-311, ' ': -1.414261190585428e+202,'\u2029ⁱNj': ' ', ' \u2029': inf, ' ': ' ',

'\u3000 ': -1.0065151140507456e+206, 'Nj ': None, ' ⁱ': None,'I\U00014481': -1.2296585975031088e+145, '\x80': ' ','\x85ⁱ \x80\x80Nj': -6.438869672267061e+116, ' ': None,'\u3000\x80': None, '\u2029\x80Nj': -698356955173.6532, ' ': ' ',' \x85': None, '\x85ⁱ\U00014481': None, ' ': None, 'ⁱ': None,' \u3000 ': ' '}

Поиск ошибокПоиск ошибок>>> @given(json_st.objects(json_st.values()))... def test_json(doc):... assert json.loads(json.dumps(doc)) == doc>>> test_json()Falsifying example: test_json(doc={'': nan})Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in test_json File "./hypothesis/core.py", line 583, in wrapped_test print_example=True, is_final=True File "./hypothesis/executors/executors.py", line 25, in default_executor

return function() File "./hypothesis/core.py", line 365, in run

return test(*args, **kwargs) File "<stdin>", line 3, in test_jsonAssertionError

Поиск ошибокПоиск ошибок>>> @given(json_st.objects(json_st.values()))... @settings(verbosity=hypothesis.Verbosity.verbose)... def test_json(doc):... assert json.loads(json.dumps(doc)) == doc>>> test_json()

Поиск ошибокПоиск ошибокTrying example: test_json(doc={})Trying example: test_json(doc={'': True})Trying example: test_json(doc={'': None})Trying example: test_json(doc={'': False})Trying example: test_json(doc={'': -43203256341979896423482879160843})Trying example: test_json(doc={'': 24}) ...Trying example: test_json(doc={'': 100440})Trying example: test_json(doc={'': 30323947834323202215971170911015})Trying example: test_json(doc={'': 0.0})Trying example: test_json(doc={'': inf})Trying example: test_json(doc={'': -inf})Successfully shrunk example 27 timesFalsifying example: test_json(doc={'': nan})

Исправление ошибокИсправление ошибокimport mathfrom hypothesis import strategies as st

def numbers():return st.integers() | st.floats().filter(math.isfinite)

Property-тестированиеProperty-тестирование@hypothesis.given(st.lists(st.integers()))def test_qsort(l): ls = qsort(l)

for i in ls:assert i in l

l.remove(i)assert len(l) == 0

assert all(ls[i - 1] <= ls[i] for i in range(1, len(ls)))

Property-тестированиеProperty-тестированиеl = [0]

@hypothesis.given(st.lists(st.integers()))def test_qsort(l):

ls = qsort(l)

for i in ls:assert i in l

l.remove(i)> assert len(l) == 0E assert 1 == 0E + where 1 = len([0])

foo.py:18: AssertionError---------------- Hypothesis ----------------Falsifying example: test_qsort(l=[0, 0])

База примеровБаза примеровHypothesis сохраняет найденные ошибки в SQLite

базу для последующего воспроизведения.

Health CheckHealth CheckГенерируемые данные слишком большиеСлишком строгая фильтрацияСлишком медленная стратегияИспользование random вызовов в кодеТест возвращает результат

Гибкая настройкаГибкая настройкаКоличество найденных успехных примеров(example)Количество итераций на тестГлубина поиска минимального примераСлишком медленная стратегияВремя выполнения тестаQA контрольПрофили

ПрофилиПрофилиimport osfrom hypothesis import settingsfrom hypothesis import Verbosity

settings.register_profile("ci", settings(max_examples=1000))settings.register_profile("dev", settings(max_examples=10))settings.register_profile("debug", settings(max_examples=10, verbosity=Verbosity.verbose))settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'default'))

Что хорошо тестируетсяЧто хорошо тестируетсяАлгоритмыСтруктуры данных любой сложностиРеализации протоколов, парсеров, базы данныхЛюбые чистые функцииДетерминированные API

Полезные ссылкиПолезные ссылкиHypothesishttps://github.com/DRMacIver/hypothesisHypothesis Talkshttps://github.com/DRMacIver/hypothesis-talksConjecturehttps://github.com/DRMacIver/conjecturePYCON UK 2015: Finding more bugs with less workhttps://www.youtube.com/watch?v=62ubHXzD8tMHow I handled Erlang R18 Maps with QuickCheckhttps://vimeo.com/143849945СurEr - Concolic Testinghttps://www.youtube.com/watch?v=XVOV0KQAf-8

Спасибо за внимание!Спасибо за внимание!