Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Существуют три наисложнейшие проблемы в программировании: именование, кэширование и выход за границу массива. Проверка пограничных случаев поведения кода наиболее важна, но эта зона так же наименее тестируема. Придумать и предугадать все возможные ситуации человеку тяжело и порой мы всегда что-то упускаем из виду. Вот было бы здорово, если бы тесты сами находили такие случаи, при которых наш код падает....Мечты? О том, как превратить мечты в реальность этом докладе.

Moscow Python Meetup

March 22, 2016
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. QuickCheck в Python QuickCheck в Python Проверка гипотез и поиск

    ошибок Проверка гипотез и поиск ошибок Шорин Александр / @kxepal
  2. О чем пойдет речь: О чем пойдет речь: Что не

    так с нашими тестами Что такое QuickCheck... ...и как его неправильно портировать Знакомство с Hypothesis Время офигенных историй
  3. Простейший пример Простейший пример 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 (да, в жизни все сложнее) (да, в жизни все сложнее)
  4. Тестируем Тестируем 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] Шаблонная копипаста Шаблонная копипаста
  5. Мы можем решить проблему копипасты... Мы можем решить проблему копипасты...

    @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
  6. ...хотя может показаться, что нет ...хотя может показаться, что нет

    @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
  7. Можем ли мы проверить все допустимые случаи? Можем ли мы

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

    мы так и не нашли нашли 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: 1 E Full diff: E - [1] E + [1, 1, 1]
  9. Резюмируем Резюмируем Мы не тестируем наш код, мы закрепляем его

    поведение Мы не способны описать все пограничные случаи Баги всегда впереди нас
  10. QuickCheck QuickCheck Изначально написан на Haskell Основывается на научно-исследовательских работах

    Позиционируется как библиотека для автоматического тестирование функций на основе спецификаций и свойств данных Портирован на Scala, Erlang, Clojure, JavaScript... Продвигается компанией Quviq http://www.cse.chalmers.se/~rjmh/QuickCheck/
  11. Как оно работает (примерно) Как оно работает (примерно) passed +-----------------------+

    v | +-----------+ sample +------+ failed +----------+ +--------+ | Generator | --------> | Test | --------> | Shrinker | --> | Report | +-----------+ +------+ +----------+ +--------+ | ^ sample | ^ | +------------------+ | | | +-----------------------------------------------------------+ success
  12. Нельзя просто взять и написать Нельзя просто взять и написать

    правильный QuickCheck правильный QuickCheck https://github.com/agrif/pyquickcheck https://github.com/Cue/qc https://github.com/dbravender/qc https://github.com/futoase/PyQCheck https://github.com/JesseBuesking/pythoncheck https://github.com/markchadwick/paycheck https://github.com/msoedov/quick.py https://github.com/npryce/python-factcheck https://github.com/Xion/pyqcy https://github.com/zombiecalypse/qcc https://pypi.python.org/pypi/pytest-quickcheck ...
  13. Типичные ошибки Типичные ошибки Слепое копирование Haskell реализации Использование типов

    в качестве генераторов Отсутствующий или же "глупый" shrinking Заброшенные или же в зачаточном состоянии
  14. Что умеет Что умеет Генерация всех основных типов данных Рекурсивные

    типы State Machine, N-ary деревья Позволяет создавать свои стратегии без погружения в детали Интегрирован с Fake Factory, Django, pytest Следит за качеством тестов Запоминает найденные баги Хорошо настраивается http://hypothesis.readthedocs.org/en/master/data.html http://hypothesis.readthedocs.org/en/master/data.html
  15. Генерация данных Генерация данных 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)))
  16. Генерация данных Генерация данных >>> 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 ': ' '}
  17. Поиск ошибок Поиск ошибок >>> @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_json AssertionError
  18. Поиск ошибок Поиск ошибок 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 times Falsifying example: test_json(doc={'': nan})
  19. Исправление ошибок Исправление ошибок import math from hypothesis import strategies

    as st def numbers(): return st.integers() | st.floats().filter(math.isfinite)
  20. 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)))
  21. 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) == 0 E assert 1 == 0 E + where 1 = len([0]) foo.py:18: AssertionError ---------------- Hypothesis ---------------- Falsifying example: test_qsort(l=[0, 0])
  22. Health Check Health Check Генерируемые данные слишком большие Слишком строгая

    фильтрация Слишком медленная стратегия Использование random вызовов в коде Тест возвращает результат
  23. Гибкая настройка Гибкая настройка Количество найденных успехных примеров (example) Количество

    итераций на тест Глубина поиска минимального примера Слишком медленная стратегия Время выполнения теста QA контроль Профили
  24. Профили Профили import os from hypothesis import settings from 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'))
  25. Что хорошо тестируется Что хорошо тестируется Алгоритмы Структуры данных любой

    сложности Реализации протоколов, парсеров, базы данных Любые чистые функции Детерминированные API
  26. Полезные ссылки Полезные ссылки Hypothesis https://github.com/DRMacIver/hypothesis Hypothesis Talks https://github.com/DRMacIver/hypothesis-talks Conjecture

    https://github.com/DRMacIver/conjecture PYCON UK 2015: Finding more bugs with less work https://www.youtube.com/watch?v=62ubHXzD8tM How I handled Erlang R18 Maps with QuickCheck https://vimeo.com/143849945 СurEr - Concolic Testing https://www.youtube.com/watch?v=XVOV0KQAf-8