39
Лекция 10: Тестирование, документирование, дистрибуция Фёдор Гоголев 3 декабря 2014 г.

Python, осень 2014: Документация и тестирование

Embed Size (px)

Citation preview

Page 1: Python, осень 2014: Документация и тестирование

Лекция 10: Тестирование, документирование,

дистрибуция

Фёдор Гоголев

3 декабря 2014 г.

Page 2: Python, осень 2014: Документация и тестирование

Тестирование

Page 3: Python, осень 2014: Документация и тестирование

Тестирование: обзор

• Мы будем говорить о модульном тестировании.

• Тестировать, чтобы проверять.

• Тестировать, чтобы улучшать.

• Некоторые тестируют, чтобы писать.

• Модуль unittest.

• В стандартной библиотеке.

• Клон JUnit, до 2001 года назывался PyUnit.

• Модуль doctest.

• В стандартной библиотеке.

• Подходит для совсем небольших проектов.

1 / 35

Page 4: Python, осень 2014: Документация и тестирование

Почему не assert

def cut_suffix(s, suffix):

if s.find(suffix) > 0:

s = s.rstrip(suffix)

return s

assert cut_suffix("abcd", "cd") == "ab", "positive"

assert cut_suffix("abcd", "abcd") == "", "whole"

assert cut_suffix("abcdcd", "cd") == "abcd", "repeat"

Traceback (most recent call last):

File "test.py", line 7, in <module>

assert cut_suffix("abcd", "abcd") == "", "whole"

AssertionError: whole

• Прерывается выполнение после первого AssertionError

• Практически отсутствует информация об ошибке

2 / 35

Page 5: Python, осень 2014: Документация и тестирование

Модуль unittest

import unittest

def cut_suffix(s, suffix):

if s.find(suffix) > 0:

s = s.rstrip(suffix)

return s

class TestCutSuffix(unittest.TestCase):

def test_positive(self):

self.assertEqual(cut_suffix("abcd", "cd"), "ab")

def test_positive_whole(self):

self.assertEqual(cut_suffix("abcd", "abcd"), "")

def test_suffix_repeat(self):

self.assertEqual(cut_suffix("abcdcd", "cd"), "abcd")

if __name__ == "__main__":

unittest.main()

3 / 35

Page 6: Python, осень 2014: Документация и тестирование

$ python cut_suffix_test.py

.FF

======================================================================

FAIL: test_positive_whole (__main__.TestCutSuffix)

----------------------------------------------------------------------

AssertionError: 'abcd' != ''

- abcd

+

======================================================================

FAIL: test_suffix_repeat (__main__.TestCutSuffix)

----------------------------------------------------------------------

AssertionError: 'ab' != 'abcd'

- ab

+ abcd

? ++

----------------------------------------------------------------------

Ran 3 tests in 0.001s

FAILED (failures=2)

4 / 35

Page 7: Python, осень 2014: Документация и тестирование

Методы класса unittest.TestCase

• assertEqual(x, y) / assertNotEqual(x, y)

• assertTrue(x) / assertFalse(x)

• assertIs(x, y) / assertIsNot(x, y)

• assertIsNone(x) / assertIsNotNone(x)

• assertIn(x, collection) / assertNotIn(x, collection)

• assertIsInstance(obj, cls) / assertNotIsInstance(obj, cls)

• Есть специализированные методы для встроенныхколлекций и регулярных выражений, например:

• assertDictEqual(x, y)

• assertSetEqual(x, y)

• assertRegex(pattern, s)

• assertAlmostEqual(x, y, places=5)

5 / 35

Page 8: Python, осень 2014: Документация и тестирование

>>> from unittest import TestCase

>>> TestCase().assertTrue("baz" in ["foo", "bar"])

...

AssertionError: False is not true

>>> TestCase().assertIn("baz", ["foo", "bar"])

...

AssertionError: 'baz' not found in ['foo', 'bar']

>>> TestCase().assertIs(0, True)

...

AssertionError: 0 is not True

>>> TestCase().assertSetEqual({"foo", "bar"}, {"foo", "baz"})

...

AssertionError: Items in the first set but not the second:

'bar'

Items in the second set but not the first:

'baz'

>>> TestCase().assertAlmostEqual(0.001, 0.002, places=2)

>>> TestCase().assertAlmostEqual(0.001, 0.002, places=3)

...

AssertionError: 0.001 != 0.002 within 3 places

6 / 35

Page 9: Python, осень 2014: Документация и тестирование

Исключения в unittest

Класс TestCase передоставляет метод assertRaises для работы с

исключениями.

def fail():

return 1 / 0

class FailTest(unittest.TestCase):

def test_fail(self):

self.assertRaises(ZeroDivisionError, fail)

def test_fail_manager(self):

with self.assertRaises(Exception) as exc_manager:

fail()

self.assertIsInstance(exc_manager.exception,

ZeroDivisionError)

7 / 35

Page 10: Python, осень 2014: Документация и тестирование

unittest: методы setUp и tearDown

• Иногда необходимо тестировать что-то, у чего есть

состояние. В таком случае надо инициализировать это

состояние отдельно для каждого теста. В этом помогают

методы setUp и tearDown класса TestCase:

• Например, протестируем класс Bits:

class TestBits(unittest.TestCase):

def setUp(self): # Вызывается до запуска каждого теста

self.bits = Bits(32)

def tearDown(self): # Вызывается после выполнения

pass # каждого теста

def test_bits_true(self):

for idx in range(len(self.bits))

self.bits[idx] = True

self.assertNotIn(False, self.bits)

def test_bits_false(self):

self.assertNotIn(True, self.bits)

8 / 35

Page 11: Python, осень 2014: Документация и тестирование

Модуль doctest — тесты в документации

import doctest

def cut_suffix(s, suffix):

"""

Функция возвращает строку без суффикса.

>>> cut_suffix("abcd", "cd")

'ab'

>>> cut_suffix("abcd", "abcd")

''

>>> cut_suffix("abcdcd", "cd")

'abcd'

"""

if s.find(suffix) > 0:

s = s.rstrip(suffix)

return s

if __name__ == "__main__":

doctest.testmod()

9 / 35

Page 12: Python, осень 2014: Документация и тестирование

$ python cut_suffix_test.py

*************************************************

File "__main__", line 7, in __main__.cut_suffix

Failed example:

cut_suffix("abcd", "abcd")

Expected:

''

Got:

'abcd'

*************************************************

File "__main__", line 9, in __main__.cut_suffix

Failed example:

cut_suffix("abcdcd", "cd")

Expected:

'abcd'

Got:

'ab'

*************************************************

1 items had failures:

2 of 3 in __main__.cut_suffix

***Test Failed*** 2 failures.

TestResults(failed=2, attempted=3)

10 / 35

Page 13: Python, осень 2014: Документация и тестирование

doctest: исключения и состояние

def fail(n):

"""

Всего лишь падает с ZeroDivisionError.

>>> n = 5

>>> fail(n)

Traceback (most recent call last):

...

ZeroDivisionError: division by zero

"""

return n / 0

• doctest работает со строковым выводом интерпретатора.

• Можно считать, что для каждого докстринга будет запущен

свой интерпретатор, все строки используют общее

пространство имён.

11 / 35

Page 14: Python, осень 2014: Документация и тестирование

Лучшие системы

• Проблемы unittest:

• Общие претензии: неродной синтаксис и многословный.

• До недавнего времени был исключительно ограничен в

возможностях.

• Удивительно трудно найти все тесты для крупного проекта.

• Альтернативы:

• pytest — очень «магический», огромное количество

возможностей, сложнее прочих.

• nosetests — вырос из pytest, значительно проще, меньше

возможностей.

• Множество плагинов для каждой альтернативы.

• Обе альтернативы успешно запускают тесты от unittest и

doctest.

12 / 35

Page 15: Python, осень 2014: Документация и тестирование

pytest: пример

@pytest.mark.parametrize("left, right",

[("foo", "boo"), ({1, 2}, {2, 3})])

def test_equality(left, right):

assert left == right

$ py.test test_something.py

___________ test_equality[foo-boo] ___________

left = 'foo', right = 'boo'

> assert left == right

E assert 'foo' == 'boo'

E - foo

E + boo

_________ test_equality[left1-right1] _________

left = set([1, 2]), right = set([2, 3])

> assert left == right

E assert set([1, 2]) == set([2, 3])

E Extra items in the left set:

E 1

E Extra items in the right set:

E 3

=========== 2 failed in 0.02 seconds ===========13 / 35

Page 16: Python, осень 2014: Документация и тестирование

Тестирование: резюме

• Стандартная библиотека Python предоставляет несколько

инструментов для работы с тестами.

• Класс unittest.TestCase содержит десятки методов для

различных тестов, которые в первую очередь нужны для

удобной визуализации ошибок.

• Для небольших проектов стоит использовать doctest.

• Вы можете дополнительно посмотреть на альтернативные

системы тестирования: pytest и nosetests.

14 / 35

Page 17: Python, осень 2014: Документация и тестирование

Документирование

Page 18: Python, осень 2014: Документация и тестирование

Документирование: история

• Документация в Python пишется в докстрингах модуля,

класса, функции, доступна через аттрибут __doc__ и может

быть выведена, например, с помощью встроенной

функции help.

• В 2001 году был написан сервер pydoc, который

анализировал объекты и генерировал соответствующий

HTML, до сих пор доступный в виде модуля.

• Ещё через два года появился автоматический генератор

API документации Epydoc, который поддерживал

Javadoc-style докстринги, аннотации типов, ссылки на

объекты и т. д.

• Сам Python к этому времени всё ещё использовал LATEXи не

было особой связи между докстрингами в коде и итоговой

документацией.

• С версии 2.6 (2008 год) Python перешёл на генератор

документации Sphinx и на reStructuredText, которые

теперь стали de facto стандартом документации.15 / 35

Page 19: Python, осень 2014: Документация и тестирование
Page 20: Python, осень 2014: Документация и тестирование
Page 21: Python, осень 2014: Документация и тестирование

Sphinx

• Sphinx — основная на данный момент система

документирования для Python.

• Использует язык разметки reStructuredText (rst, rest),

специально созданный для документирования Python.

• Поддерживает большое количество форматов вывода.

Используется, как правило, HTML.

• Поддерживает огромное количество расширений, включая

систему коллекции документации из докстрингов.

• Принципиально поддерживает разные языки

программирования. Но не получил широкого

распространения нигде кроме Python.

18 / 35

Page 22: Python, осень 2014: Документация и тестирование

def double_hash(k, value, modulo):

"""

Функция возвращает список чисел не больше ``modulo``,

являющихся результатом применения ``k`` хеш

функций к ``value``.

`Подробнее <http://bit.ly/double-hashing>`_.

:param int k: количество хеш-функций, которые необходимо

построить

:param int value: значение

:param int modulo: модуль

:rtype: int

"""

h1 = hash(abs(value))

h2 = (h1 >> 32) & sys.maxsize

return [(h1 + (h2 + 1) * i) % modulo for i in range(k)]

19 / 35

Page 23: Python, осень 2014: Документация и тестирование

reStructuredText: Разметка

• Инлайн разметка: *курсивный*, **жирный**,

``моноширинный``

• Группировка:Заголовок

=========

Подзаголовок

------------

• Списки:* Ненумерованный список

* Второй элемент спика

#. Вложенный нумерованный список

• Внешние ссылки:

`Текст ссылки <http://compscicenter.ru/>`_

• Внутренние ссылки:• Метка в документации: .. _label-name:

• Ссылка на метку: :ref:`Текст ссылки <label-name>`

• Ссылка на Python объект: :func:`double_hash`,

:class:`BloomFilter`, :mod:`string`

20 / 35

Page 24: Python, осень 2014: Документация и тестирование

reStructuredText: Разметка

• Параграфы отбиваются друг от друга одной или

несколькими пустыми строками.

• Блоки кода выделяются маркером :: и должны быть

идентированы.

• Подробнее о разметке:

http://sphinx-doc.org/rest.html

Первый параграф, возможно, многострочный, содержит какой-то

текст.

Следующий параграф и пример блока кода::

>>> from pyte import Screen

>>> screen = Screen(80, 40)

21 / 35

Page 25: Python, осень 2014: Документация и тестирование

reStructuredText: Список определений, список полей

• Существует синтаксис для списка определений и списка

полей.

• Каждый элемент списка определений содержит термин и

определение, определение может содержать несколько

параграфов.

• Список полей удобно воспринимать как таблицу

ключ-значение, при этом отображение полей, как правило,

зависит от контекста использования.

• Списки определений и списки полей используются,

например, для аннотаций объектов.

Термин

Определение термина.

Следующий термин : классификатор : классификатор 2

Определение следующего термина.

:ключ: значение

:ключ с параметрами: значение

22 / 35

Page 26: Python, осень 2014: Документация и тестирование

Аннотации

• Речь о маркерах в документации, которые определяют:

принимаемые значения, их типы, возвращаемые значения,

возможные поднимаемые исключения.

• Подобные маркеры в формате Javadoc поддерживал ещё

Epydoc.

• Есть несколько общепринятых стандартов12 для

добавления аннотаций к документируемым объектам.

• Каждый из них воспринимается современными IDE,

обеспечивая соответствующие подсказки.

1http://bit.ly/annotate-numpy2http://bit.ly/annotate-sphinx

23 / 35

Page 27: Python, осень 2014: Документация и тестирование

Предлагаемый Shpinx

def double_hash(k, value, modulo):

"""

:param int k: количество хеш-функций, которые необходимо

построить

:param int value: значение

:param int modulo: модуль

:return: список чисел не больше ``modulo``,

являющихся результатом применения ``k`` хеш

функций к ``value``.

:rtype: int

:raises ValueError: параметр k должен быть

положительным числом

"""

24 / 35

Page 28: Python, осень 2014: Документация и тестирование

Стандарт numpy

def double_hash(k, value, modulo):

"""

Parameters

----------

k : int

количество хеш-функций, которые необходимо построить

value : int

значение

modulo : int

модуль

Returns

-------

int

список чисел не больше ``modulo``,

являющихся результатом применения ``k`` хеш

функций к ``value``.

Raises

------

ValueError

параметр k должен быть положительным числом

"""

25 / 35

Page 29: Python, осень 2014: Документация и тестирование

reStructuredText: Директивы

• reStructuredText был спроектирован как расширяемый

язык.

• Директива — основная единица расширения, некоторые изних:

• .. math:: — выражение LATEXформулы.

• .. image:: — изображение в документе

• .. csv-table:: — таблица из csv

Например, выражение

.. math::

\epsilon \approx

\left( 1 - e^{\dfrac{-k n}{m}} \right)^k.

Будет показано как:

ε ≈

1− e

−kn

m

k

.

26 / 35

Page 30: Python, осень 2014: Документация и тестирование

Sphinx: Быстрый старт

• Sphinx поставляется с программой sphinx-quickstart,

которая, после ответов на несколько вопросов, строит

стандартное дерево директорий для документации. Стоит

ответить «yes» хотя бы на вопрос о включении

расширения autodoc.

• Расширение autodoc позволяет генерировать

документацию из .py файлов так же, как это делает pydoc.

• В начале, для генерации осмысленной документации

достаточно воспользоваться программой sphinx-apidoc.

• В зависимости от ответов на вопросы, получается

примерно такое дерево директорий:

docs # Корневая директория документации

|-- _build # Скомпилированная документация

|-- conf.py # Файл настроек, в частности,

# включенные расширения

|-- index.rst # Точка входа документации

|-- pyte.rst # Файл сгенерированный sphinx-apidoc

27 / 35

Page 31: Python, осень 2014: Документация и тестирование

Sphinx: Быстрый старт

$ sphinx-quickstart

Welcome to the Sphinx 1.2.3 quickstart utility.

> Root path for the documentation [.]: docs

...

Please indicate if you want to use one of the following

Sphinx extensions:

> autodoc: automatically insert docstrings from

modules (y/n) [n]: yes

...

Finished: An initial directory structure has been created.

$ sphinx-apidoc -o docs pyte

Creating file docs/pyte.rst.

Creating file docs/modules.rst.

$ PYTHONPATH=. sphinx-build docs/ docs/_build/html

Running Sphinx v1.2.2

...

build succeeded.

28 / 35

Page 32: Python, осень 2014: Документация и тестирование

pyte.rst

• sphinx-autodoc генерирует примерно такие файлы

документации:

pyte package

============

Module contents

---------------

.. automodule:: pyte

:members:

:undoc-members:

:show-inheritance:

pyte.charsets module

--------------------

.. automodule:: pyte.charsets

:members:

:undoc-members:

:show-inheritance:

29 / 35

Page 33: Python, осень 2014: Документация и тестирование
Page 34: Python, осень 2014: Документация и тестирование

Документирование: резюме

• Подавляющее большинство Python проектов

документируются с помощью Sphinx и reStructuredText.

• Sphinx поддерживает автоматическую генерацию

документации из исходного кода.

• Мы рассмотрели базовый синтаксис reStructuredText иSphinx, за кадром остались:

• Многие встроенные директивы, например .. toctree::

для генерации оглавления.

• Множество расширений, например, автоматическое

выполнение доктестов.

• Темы и шаблоны итоговой документации.

• Другие форматы вывода: PDF, ePub, man и т. д.

31 / 35

Page 35: Python, осень 2014: Документация и тестирование

Дистрибуцияв четырёх слайдах

Page 36: Python, осень 2014: Документация и тестирование

Репозиторий

• С Python поставляется огромная стандартная библиотека.

• Однако в ней нет фильтра Блума и других полезных вещей.

• Есть репозитории Python пакетов.

• Есть центральный репозиторий —

http://pypi.python.org/, более пятидесяти тысяч

пакетов.

• Утилита, для работы с репозиторием — pip:

$ pip install sphinx

...

$ python -c "import sphinx; print(sphinx.__version__)"

1.2.3

$ pip uninstall sphinx

32 / 35

Page 37: Python, осень 2014: Документация и тестирование

Структура Python проекта

pyte # Корневая директория проекта

|-- docs/ # Директория с документацией

|-- ...

|-- pyte/ # Пакет в проекте

|-- __init__.py # Инициализатор пакета, файл

# доступен при вызове `import pyte`

|-- screen.py # Один из модулей, импортируется

# как `import pyte.screen`

|-- tests/ # Тесты для модуля

|-- __init__.py # Тесты тоже являются пакетом,

# поэтому нужен инициализатор

|-- test_config.py # Один из модулей с тестами

|-- setup.py # Файл конфигурации проекта

33 / 35

Page 38: Python, осень 2014: Документация и тестирование

setup.py

from distutils.core import setup

setup(name="pyte",

version="0.4.8",

packages=["pyte"],

tests_require=["pytest"],

install_requires=["readline>=6.2.4.1"],

platforms=["any"],

author="Sergei Lebedev",

author_email="[email protected]",

description="Simple VTXXX-compatible terminal emulator."

keywords=["vt102", "vte", "terminal emulator"],

url="https://github.com/selectel/pyte",

)

34 / 35

Page 39: Python, осень 2014: Документация и тестирование

setup.py

• С помощью setup.py (фактически distutils) можно:

• Собрать пакет.

• Протестировать пакет.

• Загрузить пакет в репозиторий.

• Выполнить тесты.

• Собрать документацию.

• Загрузить документацию.

• Например:

$ python setup.py install

running install

...

Finished processing dependencies for pyte==0.4.8

• Пакет установлен в систему.

35 / 35