Site icon 8HOST.COM

Продвинутое тестирование в Python: как писать доктесты

Тестирование кода в Python выглядит немного по-другому

Документация и тестирование – основные компоненты любого продуктивного процесса разработки программного обеспечения. Тщательное документирование и тестирование кода не только позволяет вам убедиться, что программа функционирует должным образом, но и поддерживает совместную работу программистов, а также влияет на адаптацию пользователей. Программисту может быть полезно сначала написать документацию, затем тесты, и только потом – сам код. Следуя такому процессу, вы можете быть уверены, что кодируемая функция хорошо продумана и учитывает возможные пограничные случаи.

Стандартная библиотека Python поставляется с модулем фреймворка для тестирования, он называется doctest. Модуль doctest ищет в комментариях кода Python фрагменты текста, которые выглядят как интерактивные сеансы Python. Затем модуль выполняет эти сеансы, чтобы подтвердить, что код, на который ссылается doctest, работает так, как задумано.

Кроме того, doctest генерирует для кода документацию, предоставляя примеры ввода-вывода. В зависимости от вашего подхода к написанию доктестов, она может быть ближе к грамотному тестированию или исполняемой документации (согласно Python Standard Library).

Структура доктеста

Доктесты в Python пишутся в виде обычных комментариев, но помещаются внутри двойных кавычек – нужно три символа в начале и в конце доктеста.

Иногда в доктесты включается пример функции и ожидаемого результата, иногда также нужно включить комментарий о том, для чего предназначена функция. Комментарии помогают программистам подробно описать свои цели, чтобы люди, читающие код в будущем, могли легко все понять (не стоит забывать, что таким человеком может оказаться и сам автор кода).

Ниже приведен математический пример доктеста для функции add (a, b), которая складывает два числа:

"""

Given two integers, return the sum.

>>> add(2, 3)

5

"""

В этом примере мы видим комментарий и пример функции add() с двумя целыми числами в качестве входных значений. Если в будущем вы захотите, чтобы функция могла складывать более двух целых чисел, вам нужно будет соответствующим образом изменить доктест.

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

Мы добавим в наш пример строки для двух аргументов, которые передаются в функцию, и для возвращаемого значения. В доктесте будут указаны типы данных для каждого из значений – параметра a, параметра b и возвращаемого значения (в этом случае все они являются целыми числами).

"""

Given two integers, return the sum.

:param a: int

:param b: int

:return: int

>>> add(2, 3)

5

"""

Читайте также: Типы данных в Python 3

Теперь этот доктест можно включить в функцию и протестировать.

Включение доктеста в функцию

Доктесты располагаются внутри функции после оператора def и перед самим кодом функции. Как следует из исходного определения функции, доктест будет иметь отступ в соответствии с соглашениями Python.

Эта короткая функция показывает, как включить doctest.

def add(a, b):

    """

    Given two integers, return the sum.

    :param a: int

    :param b: int

    :return: int

    >>> add(2, 3)

    5

    """

    return a + b

В этом коротком примере у нас есть только одна функция, поэтому теперь нам нужно импортировать модуль doctest и добавить оператор вызова для запуска doctest.

Давайте вставим следующие строки до и после нашей функции:

import doctest

...

doctest.testmod()

На этом этапе мы можем протестировать функцию в оболочке Python (не сохраняйте код в файле программы). Вы можете получить доступ к оболочке Python 3 в любом терминале командной строки (включая терминал IDE) с помощью команды python3 (или просто python, если вы используете виртуальную оболочку).

python3

После того, как вы нажмете Enter, вы получите примерно такой результат:

Type "help", "copyright", "credits" or "license" for more information.

>>>

Вы сможете ввести код после префикса командной строки >>>.

Полный пример кода, включая функцию add(), доктест и вызов этого доктеста, вы найдете ниже. Вы можете вставить его в свой интерпретатор Python, чтобы попробовать его в работе:

import doctest

def add(a, b):

    """

    Given two integers, return the sum.

    :param a: int

    :param b: int

    :return: int

    >>> add(2, 3)

    5

    """

    return a + b

doctest.testmod()

После запуска этого кода вы получите следующий результат:

TestResults(failed=0, attempted=1)

Это означает, что наша программа работает так, как и ожидалось.

Если вы замените строку return a + b на строку return a * b, в результате чего функция должна будет умножать целые числа и возвращать их произведение, вы получите уведомление об ошибке:

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

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

Failed example:

    add(2, 3)

Expected:

    5

Got:

    6

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

1 items had failures:

   1 of   1 in __main__.add

***Test Failed*** 1 failures.

TestResults(failed=1, attempted=1)

В вышеприведенном выводе видно, насколько бывает полезен модуль doctest – он подробно описывает, что пошло не так (a и b были умножены, а не сложены, и в итоге функция вернула не тот результат, который ожидался).

Доктест позволяет тестировать сразу несколько примеров функций. Давайте добавим еще один пример, в котором переменные a и b содержат значение 0 (не забудьте вернуть в код строку return a + b).

import doctest

def add(a, b):

    """

    Given two integers, return the sum.

    :param a: int

    :param b: int

    :return: int

    >>> add(2, 3)

    5

    >>> add(0, 0)

    0

    """

    return a + b

doctest.testmod()

Запустив этот код, мы получим следующий вывод от интерпретатора Python:

TestResults(failed=0, attempted=2)

Эти выходные данные указывают, что doctest протестировал обе функции – add(2, 3) и add(0, 0) – и обе они прошли проверку.

Давайте снова изменим программу и внесем оператор умножения * вместо оператора сложения +. Это покажет, насколько при работе с модулем doctest важны пограничные случаи: будь то сложение или умножение, второй пример add(0, 0) вернет одно и то же значение, а значит, пройдет проверку.

import doctest

def add(a, b):

    """

    Given two integers, return the sum.

    :param a: int

    :param b: int

    :return: int

    >>> add(2, 3)

    5

    >>> add(0, 0)

    0

    """

    return a * b

doctest.testmod()

Вывод будет выглядеть так:

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

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

Failed example:

    add(2, 3)

Expected:

    5

Got:

    6

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

1 items had failures:

   1 of   2 in __main__.add

***Test Failed*** 1 failures.

TestResults(failed=1, attempted=2)

Когда мы меняем программу, ошибку выдает только один из примеров. Если бы мы начали тестирование с примера add(0, 0), а не с add(2, 3), мы, возможно, не обратили бы внимания на потенциальную ошибку при изменении небольших компонентов нашей программы.

Доктесты в программных файлах

До сих пор для выполнения примеров мы использовали интерактивный терминал Python. Теперь давайте посмотрим, как  работают доктесты в программных файлах. Этот тестовый файл будет подсчитывать количество гласных в одном слове.

Мы можем импортировать и вызывать модуль doctest в программе в if __name__ == “__main__”: (в конце программного файла).

Сейчас в текстовом редакторе мы создадим новый файл counting_vowels.py:

nano counting_vowels.py

Начнем с определения функции count_vowels и передачи параметра word.

def count_vowels(word):

Прежде чем мы напишем тело функции, давайте объясним в doctest, чего именно мы ждем от этой функции.

counting_vowels.py

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

Пока все идет хорошо, мы довольно подробно все описали. Давайте только уточним тип данных параметра word и тип данных, который мы ожидаем получить в выводе. В первом случае это строка, во втором – целое число.

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

Затем нам нужно указать примеры. Выберите слово, в котором есть гласные, а затем введите его в ваш доктест.

Давайте для примера используем слово ‘Cusco’, это название города в Перу. Сколько гласных в этом слове? В английском языке гласными являются a, e, i, o и u. Следовательно, здесь две гласные  – u и o.

Сейчас мы можем добавить в программу доктест для Cusco и результат операции 2.

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

    >>> count_vowels('Cusco')

    2

Опять же, неплохо иметь в тесте несколько примеров. Вставьте еще один пример с большим количеством гласных, пусть это будет ‘Manila’, город на Филиппинах.

counting_vowels.py

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

    >>> count_vowels('Cusco')

    2

    >>> count_vowels('Manila')

    3

    """

Эти доктесты выглядят хорошо. Теперь мы можем написать программу.

Начнем с инициализации переменной total_vowels для хранения количества гласных. Затем создадим цикл for для итерации букв строки, которая находится в параметре word, после чего мы включим условный оператор, который проверит, является ли буква гласной. Цикл посчитает гласные в слове, а затем вернет общее количество гласных в переменную total_values. Без доктеста программа будет выглядеть так:

def count_vowels(word):

    total_vowels = 0

    for letter in word:

        if letter in 'aeiou':

            total_vowels += 1

    return total_vowels

Читайте также:

Затем в конец программы мы поместим оператор main и импортируем и запустим модуль doctest:

if __name__ == "__main__":

    import doctest

    doctest.testmod()

На данный момент программа имеет следующий вид:

counting_vowels.py

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

    >>> count_vowels('Cusco')

    2

    >>> count_vowels('Manila')

    3

    """

    total_vowels = 0

    for letter in word:

        if letter in 'aeiou':

            total_vowels += 1

    return total_vowels

if __name__ == "__main__":

    import doctest

    doctest.testmod()

Мы можем запустить программу с помощью команды python (или python3 – в зависимости от вашей среды):

python counting_vowels.py

Если вы использовали такой же код программы, как приведенный выше, вы не получите никаких результатов. Это означает, что все тесты пройдены. Отсутствие вывода – это хорошо, если вы запускаете программу для других целей. Если же вы запускаете ее специально для тестирования, вы можете использовать флаг -v, чтобы получить результат на экране.

python counting_vowels.py -v

В таком случае вы увидите:

Trying:

    count_vowels('Cusco')

Expecting:

    2

ok

Trying:

    count_vowels('Manila')

Expecting:

    3

ok

1 items had no tests:

    __main__

1 items passed all tests:

   2 tests in __main__.count_vowels

2 tests in 2 items.

2 passed and 0 failed.

Test passed.

Отлично! Программа прошла тест. Тем не менее, код еще не полностью оптимизирован и не учитывает пограничных случаев. Далее мы поговорим о том, как использовать тесты для улучшения кода.

Оптимизация кода с помощью doctest

На данный момент у нас есть рабочая программа, но ее код можно сделать лучше. Давайте попробуем найти пограничный случай. Например, что случится, если мы добавим гласную в верхнем регистре?

Поместите в doctest еще один пример, на этот раз давайте попробуем город ‘Istanbul’. В этом слове также три гласных, как в Manila.

Вместе с этим примером программа выглядит так:

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

    >>> count_vowels('Cusco')

    2

    >>> count_vowels('Manila')

    3

    >>> count_vowels('Istanbul')

    3

    """

    total_vowels = 0

    for letter in word:

        if letter in 'aeiou':

            total_vowels += 1

    return total_vowels

if __name__ == "__main__":

    import doctest

    doctest.testmod()

Давайте снова запустим программу.

python counting_vowels.py

Мы обнаружили пограничный случай! Ниже представлен результат тестирования:

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

File "counting_vowels.py", line 14, in __main__.count_vowels

Failed example:

    count_vowels('Istanbul')

Expected:

    3

Got:

    2

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

1 items had failures:

   1 of   3 in __main__.count_vowels

***Test Failed*** 1 failures.

Согласно этому результату, слово ‘Istanbul’ не прошло тест. Мы сказали программе, что в этом слове три гласные, но программа насчитала только две. Что пошло не так?

В коде есть строка if letter in ‘aeiou’, согласно которой программа ищет только гласные в нижнем регистре. Чтобы гласные в верхнем регистре тоже считались, нам нужно изменить строку aeiou на AEIOUaeiou. Есть и более элегантное решение этой проблемы: мы можем преобразовать значение параметра word в нижний регистр с помощью word.lower(). Давайте сделаем последнее.

counting_vowels.py

def count_vowels(word):

    """

    Given a single word, return the total number of vowels in that single word.

    :param word: str

    :return: int

    >>> count_vowels('Cusco')

    2

    >>> count_vowels('Manila')

    3

    >>> count_vowels('Istanbul')

    3

    """

    total_vowels = 0

    for letter in word.lower():

        if letter in 'aeiou':

            total_vowels += 1

    return total_vowels

if __name__ == "__main__":

    import doctest

    doctest.testmod()

Теперь все тесты должны пройти успешно. Вы можете подтвердить это, запустив команду:

python counting_vowels.py –v

Обратите внимание: программа, вероятно, не учитывает все крайние случаи, а значит, это все еще не лучшая версия кода.

К примеру, что будет, если мы передадим значение ‘Sydney’? В английском языке y иногда считается гласной. Так сколько гласных мы хотим получить в итоге – три или одну?

А что произойдет, если передать значение ‘Würzburg’ – программа должна учитывать «ü» или нет? Как вообще она должна обрабатывать слова, написанные не английским алфавитом, и слова, в которых используются разные кодировки символов, например, UTF-16 или UTF-32?

Разработчику программного обеспечения иногда придется принимать сложные решения: например, решить, какие символы следует считать гласными. В отдельных ситуациях правильного или неправильного ответа может не быть.

Модуль doctest – хороший инструмент, который поможет вам начать поиски пограничных случаев и собрать предварительную документацию. Но для создания надежных программ, которые будут учитывать широкий спектр возможностей, вам в конечном итоге все равно потребуется тестирование при участии пользователя-человека.

Заключение

В этом руководстве вы узнали, что модуль doctest можно использовать не только для тестирования и документирования программного обеспечения, но и как способ продумать программу перед тем, как начать писать код. С его помощью можно сначала задокументировать свою программу, затем протестировать ее, и только потом писать код.

Без тестирования ваше программное обеспечение будет подвержено ошибкам и сбоям. Если вы выработаете в себе привычку писать тесты еще до написания кода, вы сможете легко поддерживать программу в производительном состоянии, что одинаково полезно как другим разработчикам, так и конечным пользователям.

Читайте также: