Вы только что завершили создание своего первого приложения на Python для командной строки. Или, возможно, это уже ваше второе или третье приложение. Вы уже некоторое время изучаете Python, и теперь готовы создать что-то более крупное и сложное, но все еще предназначенное для выполнения в командной строке. Либо вы привыкли разрабатывать и тестировать веб-приложения или приложения с графическим интерфейсом (GUI), а сейчас начинаете делать приложения с интерфейсом командной строки (CLI).

Во всех этих и других ситуациях вам потребуется изучить и освоить различные методы тестирования приложений на Python CLI.

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

В этом уроке вы познакомитесь с четырьмя практическими приемами тестирования приложений командной строки Python:

  • Отладка "Lo-Fi" с помощью print()

  • Использование визуального отладчика Python

  • Юнит-тестирование с использованием pytest и mocks (имитационные объекты - моки) 

  • Интеграционное тестирование

В основе всего будет лежать базовое приложение Python CLI. Это означает, что пользователь станет взаимодействовать с приложением через командную строку, вводя команды и получая соответствующие результаты. Приложение будет получать данные в виде многоуровневого словаря. Словарь может содержать различные уровни вложенности и структуры, например, вложенные словари внутри списков и так далее. Затем, эти данные будут переданы двум функциям. Функции выполнят определенные преобразования данных в соответствии с заданной логикой. Они могут включать в себя обработку, фильтрацию, сортировку или любые другие операции, необходимые для обработки данных. После таких преобразований функции предоставят результаты обратно приложению. Результаты могут быть представлены в различных форматах, например, в виде текстовых сообщений или структурированных данных. И наконец, приложение выведет пользователю полученные результаты через интерфейс командной строки.

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

На примере приведенного ниже кода мы рассмотрим несколько различных методов, которые помогут вам в тестировании. И хотя этот учебник, конечно, не является исчерпывающим, я надеюсь, что он даст достаточный объем знаний, чтобы вы могли уверенно создавать эффективные тесты в основных областях тестирования.

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

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

Первым делом давайте представим себе наши объекты на каждом этапе работы приложения. Начнем со структуры, описывающей Джона Кью Паблика (John Q. Public):

JOHN_DATA = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

Затем мы упрощаем [трансформируем их многоуровневые структуры в более линейные формы, чтобы было легче работать с данными] другие словари, ожидая, что это произойдет после вызова нашей первой функции преобразования initial_transform:

JOHN_DATA = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'siblings': ['Michael R. Public', 'Suzy Q. Public'],
    'parents': ['John Q. Public Sr.', 'Mary S. Public'],
}

Затем с помощью функции final_transform мы собираем всю информацию об адресе в одну адресную запись:

JOHN_DATA = {
    'name': 'John Q. Public',
    'address': '123 Main St. \nAnytown, FL 99999'
    'siblings': ['Michael R. Public', 'Suzy Q. Public'],
    'parents': ['John Q. Public Sr.', 'Mary S. Public'],
}

А при вызове print_person в консоль будет записано следующее:

Hello, my name is John Q. Public, my siblings are Michael R. Public 
and Suzy Q. Public, my parents are John Q. Public Sr. and Mary S. Public, 
and my mailing address is:
123 Main St. 
Anytown, FL 99999

testapp.py:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(item) is dict:
            for key in item:
                data[key] = item[key]

    return data


def final_transform(transformed_data):
    """
    Transform address structures into a single structure
    """
    transformed_data['address'] = str.format(
        "{0}\n{1}, {2} {3}", transformed_data['street'], 
        transformed_data['state'], transformed_data['city'], 
        transformed_data['zip'])

    return transformed_data


def print_person(person_data):
    parents = "and".join(person_data['parents'])
    siblings = "and".join(person_data['siblings'])
    person_string = str.format(
        "Hello, my name is {0}, my siblings are {1}, "
        "my parents are {2}, and my mailing"
        "address is: \n{3}", person_data['name'], 
        parents, siblings, person_data['address'])
    print(person_string)


john_data = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

suzy_data = {
    'name': 'Suzy Q. Public',
    'street': '456 Broadway',
    'apt': '333',
    'city': 'Miami',
    'state': 'FL',
    'zip': 33333,
    'relationships': {
        'siblings': ['John Q. Public', 'Michael R. Public', 
                    'Thomas Z. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    }
}

inputs = [john_data, suzy_data]

for input_structure in inputs:
    initial_transformed = initial_transform(input_structure)
    final_transformed = final_transform(initial_transformed)
    print_person(final_transformed)

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


"Lo-Fi" отладка с помощью Print

Это один из самых простых способов тестирования. Все, что нужно сделать, это вывести на печать интересующую вас переменную или объект - до вызова функции, после вызова функции или внутри функции.

Соответственно, это позволяет проверить входные данные функции, выходные данные функции и логику ее работы.

Если сохранить приведенный выше код под именем testapp.py и попытаться запустить его с помощью команды python testapp.py, то можно увидеть ошибку вроде этой:

Traceback (most recent call last):
  File "testapp.py", line 60, in <module>
    print_person(final_transformed)
  File "testapp.py", line 23, in print_person
    parents = "and".join(person_data['parents'])
KeyError: 'parents'

В person_data отсутствует ключ, который передается в print_person. Первым шагом будет проверка входных данных функции print_person и выяснение того, почему не формируется ожидаемый вывод (печатное сообщение). Мы просто добавим вызов функции print перед вызовом print_person:

final_transformed = final_transform(initial_transformed)
print(final_transformed)
print_person(final_transformed)

Функция print справляется с этой задачей, показывая при выводе, что у нас нет ни верхнеуровневого ключа parents (родительские элементы), ни ключа siblings . Но в интересах логики я покажу вам функцию pprint, которая выводит многоуровневые объекты в более удобочитаемом виде. Чтобы воспользоваться этим способом, добавьте в верхнюю часть вашего скрипта команду from pprint import pprint.

Вместо print(final_transformed) мы вызываем pprint(final_transformed) для проверки нашего объекта:

{'address': '123 Main St.\nFL, Anytown 99999',
 'city': 'Anytown',
 'name': 'John Q. Public',
 'relationships': {'parents': ['John Q. Public Sr.', 'Mary S. Public'],
                   'siblings': ['Michael R. Public', 'Suzy Q. Public']},
 'state': 'FL',
 'street': '123 Main St.',
 'zip': 99999}

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

Так как мы знаем, что функция final_transform не воздействует на словарь relationships, настало время разобраться, что происходит внутри функции initial_transform. Обычно в таких ситуациях я бы использовал традиционный отладчик, чтобы шаг за шагом проанализировать код. Однако сейчас я хочу вам показать еще один способ отладки с использованием вывода на печать.

Мы можем выводить состояние объектов в виде кода, но только этим дело не ограничивается. Выводить на печать всё можно все, что угодно, и таким образом следить за ходом выполнения программы. Также можно вывести маркеры, чтобы посмотреть, какие и когда выполняются логические схемы.

Поскольку функция initial_transform в основном состоит из нескольких циклов, а внутренние словари обрабатываются вложенным циклом for, было бы полезно разобраться, что происходит внутри него. Так мы сможем выяснить, происходит ли там что-то важное и интересное.

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(item) is dict:
            print "item is dict!"
            pprint(item)
            for key in item:
                data[key] = item[key]

    return data

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

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

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

Подведение итогов

Когда стоит использовать отладку с помощью вывода на печать:

  • Простые объекты

  • Короткие скрипты

  • Ошибки, кажущиеся простыми 

  • Быстрые проверки

Подробно:

  • pprint — придание более приятного визуально или удобного для восприятия внешнего вида выводу данных. Вывод с использованием pprint будет красиво отформатированным, с отступами, разделителями и переносами строк, делая структуру данных легко читаемой.

Плюсы:

  • Быстрое тестирование

  • Простота использования

Минусы:

  • Часто требуется запуск всей программы, иначе:

  • Необходимо добавлять дополнительный код для того, чтобы контролировать порядок выполнения кода в программе вручную

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


Использование отладчика

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

Существует множество отладчиков, и часто они включены в интегрированные среды разработки (IDE). У Python есть модуль pdb, который можно использовать в интерактивной оболочке REPL ("Read-Eval-Print Loop", что в переводе означает "цикл чтения-вычисления-вывода") для отладки кода. Вместо того чтобы вдаваться в подробности реализации всех доступных отладчиков, в этом разделе я покажу, как использовать отладчики с общими функциями, такими как настройка breakpoints (точек останова) и watches (наблюдение).

Breakpoints — это специальные маркеры или инструкции в вашем коде, которые говорят отладчику, где остановить выполнение программы, чтобы вы могли тщательно изучить текущее состояние вашего приложения. Watches — это выражения, которые вы можете добавлять во время сеанса отладки, чтобы следить за значением переменных (и не только).

Но давайте вернемся к точкам останова. Они добавляются в тех местах, где необходимо начать или продолжить сеанс отладки. Поскольку мы отлаживаем метод initial_transform, мы хотим поставить одну из них именно там. Я буду обозначать точку останова символом (*):

def initial_transform(data):
    """
    Flatten nested dicts
    """
(*) for item in list(data):
        if type(item) is dict:
            for key in item:
                data[key] = item[key]

    return data

Когда мы начинаем отладку, выполнение программы приостанавливается на этой строке, и вы сможете видеть переменные и их типы в этой конкретной точке выполнения программы. У нас есть несколько опций для навигации по коду: step over (шаг вперед), step in (шаг внутрь) и step out (шаг наружу) — наиболее распространенные.

step over — это команда, которую вы будете использовать чаще всего. Она просто переходит к следующей строке кода.

step in попытка углубиться в код. Вы можете использовать это, когда сталкиваетесь с вызовом функции, который хотите исследовать более подробно. Здесь вы перейдете непосредственно к коду этой функции и получите возможность исследовать состояние уже там. Ее также часто используют, путая со step over. К счастью, step out может прийти на помощь - он возвращает нас обратно к вызывающей функции.

Мы также можем установить здесь watch, например, type(item) is dict. Это можно сделать в большинстве IDE с помощью кнопки 'add watch' во время сеанса отладки. Теперь в коде будет отображаться True или False, независимо от того, где вы находитесь.

Установите “наблюдение”, а затем сделайте "шаг вперед" так, чтобы остановиться на строке if type(item) is dict:. Теперь вы должны видеть состояние наблюдения, новую переменную item и объект data.

Даже без использования наблюдения мы можем заметить проблему: вместо того, чтобы функция type рассматривала, на что указывает переменная item, она на самом деле проверяет тип самой переменной item, которая является строкой. В конечном итоге, компьютеры выполняют только то, что мы им говорим. Благодаря отладчику, мы видим ошибку в нашем коде и исправляем её следующим образом:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = item[key]

    return data

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

john_data = {
    'name': 'John Q. Public',
    'street': '123 Main St.',
    'city': 'Anytown',
    'state': 'FL',
    'zip': 99999,
    'relationships': {
        'siblings': ['Michael R. Public', 'Suzy Q. Public'],
        'parents': ['John Q. Public Sr.', 'Mary S. Public'],
    },
    'siblings',
    'parents',
}

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

Мы уже поговорили о визуальном отладчике. И использовали его. Нам он понравился. Однако у этой техники есть свои плюсы и минусы, и вы можете ознакомиться с ними в разделе ниже.

Подведение итогов

Когда следует использовать отладчик Python:

  • Более сложные проекты

  • Сложно обнаружить ошибки

  • Необходимо проверить более одного объекта

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

Подробно:

  • Условные точки останова (брейкпоинты)

  • Оценка выражений во время отладки

Плюсы:

  • Контроль над ходом выполнения программы

  • Обзор состояния приложения с высоты птичьего полета

  • Нет необходимости точно знать место возникновения ошибки

Минусы:

  • Сложно вручную следить за очень большими объектами

  • Отладка длинного кода займет очень много времени


Юнит-тестирование с помощью Pytest и Mocks

Когда дело доходит до тестирования, предыдущие методы могут оказаться утомительными и даже требовать изменений в коде. Особенно, если вы стремитесь исчерпывающе протестировать комбинации ввода и вывода, а также покрыть все возможные ветви вашего кода. В нашем примере, результат выполнения initial_transform всё ещё выглядит не так, как бы нам хотелось.

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

На помощь приходят юнит-тесты.

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

Суть в том, что вы создаёте набор скриптов для проверки каждого метода с разными входными данными. Это позволяет убедиться, что каждая логическая ветвь внутри всех методов протестирована. Этот процесс называется покрытием кода, и обычно все стремятся к 100% покрытию. Впрочем, это не всегда практично и обязательно, но об этом можно поговорить отдельно в другой статье (или учебнике).

Во время теста каждый метод рассматривается изолированно: внешние вызовы переопределяются с помощью техники, называемой mocking (мокирование), чтобы обеспечить точные возвращаемые значения. И после выполнения теста все временные объекты и состояния удаляются. Эти и другие приемы применяются для обеспечения независимости и изолированности тестируемого модуля.

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

Pytest

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

Хорошей практикой является помещение всех тестов в директорию test внутри вашего проекта. Для нашего небольшого скрипта будет достаточно файла test_testapp.py, размещенного рядом с testapp.py.

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

Давайте начнем с настройки фикстуры. Просто посмотрите на код, и задумайтесь о тестовых случаях, которые понадобятся, чтобы покрыть все возможные ветви логики в функции initial_transform:

import pytest
import testapp as app

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request):

Прежде чем мы сгенерируем входные данные, давайте разберемся, что здесь происходит, чтобы не запутаться.

Во-первых, мы используем декоратор @pytest.fixture, чтобы объявить, что следующее определение функции является фикстурой. К тому же мы вводим именованный параметр params, который будет использован с функцией generate_initial_transform_parameters.

Интересной особенностью является то, что каждый раз, когда используется декорированная функция, она будет вызываться со всеми параметрами. Таким образом, просто вызов функции generate_initial_transform_parameters вызовет ее дважды: сначала с nodict в качестве параметра, а затем с dict.

Для доступа к этим параметрам мы добавляем специальный объект request из pytest в сигнатуру нашей функции.

Теперь давайте соберем наши входные данные и то, что мы ожидаем получить в результате:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request):
    test_input = {
        'name': 'John Q. Public',
        'street': '123 Main St.',
        'city': 'Anytown',
        'state': 'FL',
        'zip': 99999,
    }
    expected_output = {
        'name': 'John Q. Public',
        'street': '123 Main St.',
        'city': 'Anytown',
        'state': 'FL',
        'zip': 99999,
    }

    if request.param == 'dict':
        test_input['relastionships'] = {
            'siblings': ['Michael R. Public', 'Suzy Q. Public'],
            'parents': ['John Q. Public Sr.', 'Mary S. Public'],
        }
        expected_output['siblings'] = ['Michael R. Public', 'Suzy Q. Public']
        expected_output['parents'] = ['John Q. Public Sr.', 'Mary S. Public']

    return test_input, expected_output

Здесь нет ничего удивительного: мы задаем входные данные и предполагаемый результат. А если у нас есть параметр 'dict', то мы изменяем входные данные и ожидаемый результат, что дает нам возможность протестировать блок if.

Затем мы приступаем к написанию теста. В этом тесте нам нужно передать фикстуру в качестве параметра в функцию теста, чтобы получить к ней доступ:

def test_initial_transform(generate_initial_transform_parameters):
    test_input = generate_initial_transform_parameters[0]
    expected_output = generate_initial_transform_parameters[1]
    assert app.initial_transform(test_input) == expected_output

При написании тестовых функций важно помнить о нескольких ключевых моментах. Во-первых, название тестовой функции должно начинаться с префикса test_. Это сигнализирует о том, что данная функция представляет собой тест и будет проверять какой-либо аспект кода. Кроме того, тестовые функции должны строиться на базе операторов assert. С их помощью мы утверждаем, что ожидаемый результат совпадает с тем, что мы получаем, прогоняя входные данные через нашу функцию.

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

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

Моки (Mocks) 

Моки

Моки (Mocks) — это концепция, используемая в тестировании программного обеспечения, особенно в юнит-тестировании. Это специальные объекты или компоненты, которые создаются с целью имитирования поведения реальных объектов или функций в вашем коде. Поскольку мы проверяем только один отдельный блок (юнит) кода, наш интерес ограничивается его поведением, без особого внимания к тому, как работают другие вызовы функций. Нам важно получить надежные результаты именно от той части кода, которую мы тестируем.

Давайте добавим вызов внешней функции в initial_transform:

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = data[item][key]
            data.pop(item)

    outside_module.do_something()
    return data

Мы не хотим выполнять реальные запросы к функции do_something(), поэтому вместо этого мы создадим "мок" (имитацию) в нашем тестовом скрипте. Мок перехватит этот вызов и вернет результат, который мы предварительно для него установим. Я предпочитаю настраивать такие имитации в специальных "фикстурах", так как это часть подготовки к тестированию, и мы можем собрать весь этот код подготовки в одном месте:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request, mocker):
    [...]
    mocker.patch.object(outside_module, 'do_something')
    mocker.do_something.return_value(1)
    [...]

Теперь, при каждом вызове initial_transform, вызов функции do_something будет перехвачен, и вместо реального вызова будет возвращено значение 1. Также вы можете использовать параметры фикстуры, чтобы управлять тем, что будет возвращать ваш мок. Это полезно, когда ветвление в коде зависит от результата внешнего вызова.

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

def initial_transform(data):
    """
    Flatten nested dicts
    """
    for item in list(data):
        if type(data[item]) is dict:
            for key in data[item]:
                data[key] = data[item][key]
            data.pop(item)

    outside_module.do_something()
    outside_module.do_something()
    return data

Мы настроим наш мок следующим образом, передав список выводов (для каждого последующего вызова) в параметр side_effect:

@pytest.fixture(params=['nodict', 'dict'])
def generate_initial_transform_parameters(request, mocker):
    [...]
    mocker.patch.object(outside_module, 'do_something')
    mocker.do_something.side_effect([1, 2])
    [...]

Создание моков — это очень мощный инструмент. Он настолько мощный, что вы даже можете создавать mock-серверы для тестирования сторонних API. Я снова хочу вас побудить к тому, чтобы провести более подробное исследование мокирования, используя mocker.

Подведение итогов

Когда следует использовать фреймворки для юнит-тестирования на Python:

  • Для больших и сложных проектов.

  • В случае проектов с открытым исходным кодом (OSS).

  • Когда требуется автоматизировать процесс тестирования.

Полезные инструменты:

Плюсы использования фреймворков:

  • Автоматизация запуска тестов.

  • Способность выявлять разные типы ошибок.

  • Простая настройка и изменение для команды разработчиков.

Минусы использования фреймворков:

  • Необходимость написания дополнительного кода (тестов).

  • Требование обновления тестов при изменениях в коде.

  • Тесты не могут полностью воссоздать реальное выполнение приложения.


Интеграционное тестирование

Интеграционное тестирование — это один из самых простых методов проверки, но, пожалуй, самый важный. Суть его заключается в том, чтобы полностью запустить ваше приложение с реальными данными в среде, которая похожа на окружение продакшна.

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

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

Как проводить интеграционное тестирование в значительной степени зависит от вашего приложения. Например, наше тестовое приложение можно запустить самостоятельно с помощью команды python testapp.py. Однако предположим, что ваш код является частью сложного распределенного приложения, например, ETL-пайплайна. В этом случае вам потребуется запустить всю систему на тестовых серверах с вашим новым кодом, пропустить через нее данные и удостовериться, что они проходят через всю систему в правильной форме. Помимо командной строки для интеграционного тестирования вы можете использовать инструменты вроде pyVows, если ваше приложение на базе Django.

Подведение итогов

Когда использовать интеграционное тестирование на Python:

  • Всегда ;-) 

  • Обычно после применения других методов тестирования, если они использовались

Полезные инструменты:

  • Среда tox и управление автоматизацией тестирования

Плюсы использования:

  • Позволяет увидеть, как ваше приложение работает в реальных условиях

Минусы использования:

  • В случае больших приложений сложно точно отслеживать поток данных

  • Требуется наличие тестовых сред, максимально приближенных к продакшн-окружениям


Подведем итоги всего вышесказанного

В заключение следует отметить, что все тестирование через командную строку сводится к тому, чтобы сравнивать ожидаемые результаты с тем, что фактически получается при использовании определенных входных данных. Мы рассмотрели различные методы для этого, и они, во многих случаях, дополняют друг друга. Данные методы будут вам полезны при разработке приложений с интерфейсом командной строки на Python, но это руководство — лишь отправная точка.

Python обладает богатой экосистемой инструментов для тестирования, и это стоит учитывать. Не стесняйтесь исследовать и знакомиться с другими инструментами и методиками — возможно, вы найдете что-то, о чем я здесь не упомянул, но что сможет вам пригодится. Если такое произойдет, обязательно поделитесь своим опытом в комментариях!

В качестве краткого напоминания, вот техники, о которых мы сегодня говорили, и как их применять:

  • Отладка с помощью вывода на печать: распечатка переменных и маркеров в коде, чтобы увидеть, как протекает выполнение программы

  • Отладчики: управление работой программы для получения представления о состоянии приложения и ходе выполнения программы 

  • Юнит-тестирование: разбиение приложения на независимые блоки (юниты) и проверка всех возможных вариантов выполнения внутри этих блоков

  • Интеграционное тестирование: тестирование изменений в контексте всего приложения

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

Всех, дочитавших до конца, приглашаем на открытый урок 22 августа, посвященный Robot Framework. На этом занятии поговорим о том, что такое BDD и BDT, в чем преимущества и недостатки такого подхода. Рассмотрим структуру проекта, использующего Robot Framework. Напишем свой первый тест в данном подходе. Записаться на урок можно на странице курса "Python QA Engineer".