Автоматизация на Python. Тестируем API

Давно хотел написать статью с конкретными примерами автоматизации API-тестов, видимо настал тот самый момент. Основная причина, которая побудила меня это сделать — одно из недавно пройденных собеседований. Я понял, что не все осознают важность и необходимость тестирования API. Для примера: один API тест с учетом авторизации в среднем проходит 2-3 секунды, один UI тест который проверяет тот же функционал займет 10-15 секунд в зависимости от скорости инициализации драйвера, загрузки ресурсов и т.д.

И так, давайте перейдем к делу! Первым делом вам нужно установить Python, как это сделать написано тут, с одной лишь разницей, что мы будем использовать Python 3.6 и выше. Потому смело можете ставить и 3.7, и 3.8 если он уже стал актуальной версией Python на данный момент.

Настройка проекта

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

faker # Библиотека для генерации тестовых данных
flake8 # Библиотека для проверки code-style
flake8-import-order # Плагин для библиотеки выше
pytest # Тестовый фреймворк
requests # Библиотека для отправки запросов
tox # А эта библиотека поможет нам быстро проводить валидацию наших тестов

Все это мы сохраняем в файл requirements.txt и со спокойной душой выполняем команду:

pip install -r requirements.txt

Теперь нам необходимо сконфигурировать tox, pytest и flake8 для их корректной работы. Для этого мы создаем файл tox.ini в который помещаем сначала конфигурацию tox:

[tox]
envlist = py3
skipsdist = True

[testenv]
deps = -rrequirements.txt
commands =
    flake8 ./tests
    flake8 ./api
    pytest --collect-only

И так, разберем:

  • 1 блок, 1 строка — указываем на тип дистрибутива, который будет использовать tox для создания виртуального окружения;
  • 1 блок, 2 строка — указываем, что не нужно устанавливать sdist в виртуальное окружение;
  • 2 блок, 1 строка — указываем на то, откуда брать зависимости для установки;
  • 2 блок, 2 строка — перечень команд которые будут запущены при выполнении команды tox.

Далее необходимо сконфигурировать pytest:

[pytest]
addopts = -v
testpaths = tests
  • 1 строка — добавляем дополнительный параметр для всех запусков;
  • 2 строка — указываем на путь по умолчанию, где pytest должен искать тесты.
[flake8]
ignore = D203, D101, W503, C901
exclude = .git,__pycache__,venv
application-import-names = api
import-order-style = google

На конфигурации flake8 отдельно останавливаться не буду, т.к. здесь в целом все довольно просто и понятно.

Немного о тестируемом сервисе

В качестве основного сервиса для тестирования я выбрал Restful-booker, этот сервис имеет довольно понятную документацию и отлично подходит для отработки навыков в разработке автоматизированных тестов.

Подготовка клиента

Начнем с того, что нам необходимо написать клиент для этого сайта. Клиент — это инструмент, с помощью которого можно будет обращаться в коде тестов к серверному API и упрощать взаимодействия. Для того чтобы начать описывать клиента создадим следующий файл — api/client.py

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

import requests


class RestfulBookerClient:

    _s = requests.session()
    host = None

    def __init__(self, host):
        self.host = host

В соответствии с требованиями описанными в документации к API опишем первый метод для авторизации на сервере.

    def login(self, username, password):
        data = {"username": username, "password": password}
        return self._s.post(self.host + "/auth", json=data)

Так как на сервер нужно передать мало параметров и пока мы не собираемся тестировать авторизацию — нам будет достаточно принимать оба параметра в функцию. Стоит обратить внимание, что данные в словаре мы передаем в качестве параметра json. Если же передать данные в поле data — не будет указан Content-Type: application/json, и сервер, в большинстве случаев, не распознает запрос.

Дальше упростим механизм авторизации на сервере, для этого нам нужно:

  • Авторизоваться на сервере;
  • Сохранить токен;
  • Установить токен в качестве cookie.

Предлагаю написать для этого отдельный метод, для упрощения использования после;

    def authorize(self, username, password):
        res = self.login(username, password)
        if res.status_code != 200:
            raise Exception("Unable to authorize using given credentials")
        session_token = res.json().get("token")
        cookie = requests.cookies.create_cookie("token", session_token)
        self._s.cookies.set_cookie(cookie)

Дальше напишем несколько функций для работы с бронированиями:

    def create_booking(self, data: dict):
        return self._s.post(self.host + "/booking", json=data)

    def update_booking(self, uid: int,  data: dict):
        return self._s.put(self.host + f"/booking/{uid}", json=data)

    def get_booking(self, uid: int):
        return self._s.get(self.host + f"/booking/{uid}")

Теперь приступим к инициализации тестов. В первую очередь создадим файл conftest.py в корне проекта и добавим в него следующие данные:

import pytest

from api.client import RestfulBookerClient


@pytest.fixture(scope="session")
def client():
    client = RestfulBookerClient("https://restful-booker.herokuapp.com")
    client.authorize("admin", "password123")
    return client

Тут мы описываем одну из самых могущественных вещей PyTest — фикстуры. Тут у нас простая фикстура, она исполняется перед запуском теста, возвращает нам объект класса RestfulBookerClient. После, мы можем использовать результат выполнения фикстуры в тесте — просто приняв называние функции в качестве параметра тестовой функции.

Пишем тесты

Создадим папку tests и в ней файл test_example.py. И напишем наш первый тест:

from api import random


class TestExample:

    def test_create_new_booking(self, client):
        data = random.random_booking()
        res = client.vr(client.create_booking(data), [200, 201])
        created = res.json()
        assert created.get("bookingid")
        assert created.get("booking") == data

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

from faker import Faker

faker = Faker()


def random_booking():
    return {
        "firstname": faker.first_name(),
        "lastname": faker.last_name(),
        "totalprice": faker.pyint(),
        "depositpaid": True,
        "bookingdates": {
            "checkin": faker.iso8601()[:10],
            "checkout": faker.iso8601()[:10]
        },
        "additionalneeds": faker.word(),
    }

Давайте напишем еще несколько тестов:

  • Проверка существования созданного бронирования;
  • Проверка работы обновления данных бронирования;
  • Запрос рандомного ID бронирования с сервера.

Итогом, файл с тестами принимает вид:

from random import randint

from api import random


class TestExample:

    def test_create_new_booking(self, client):
        data = random.random_booking()
        res = client.vr(client.create_booking(data), [200, 201])
        created = res.json()
        assert created.get("bookingid")
        assert created.get("booking") == data

    def test_new_booking_exists(self, client):
        data = random.random_booking()
        res = client.vr(client.create_booking(data), [200, 201])
        created = res.json()
        bookingid = created.get("bookingid")
        res = client.vr(client.get_booking(bookingid))
        exists = res.json()
        assert exists == data

    def test_update_booking(self, client):
        data = random.random_booking()
        res = client.vr(client.create_booking(data), [200, 201])
        created = res.json()
        bookingid = created.get("bookingid")
        data2 = random.random_booking()
        res = client.vr(client.update_booking(bookingid, data2))
        updated = res.json()
        assert updated == data2

    def test_not_existing_booking(self, client):
        res = client.get_booking(randint(10000, 99999))
        assert res.status_code == 404

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

Также, помимо UI тестов разберем как можно оптимизировать, улучшить и, возможно, упростить код в тех или иных местах. Без усложнения отдельных моментов конечно тоже никуда — но чем сложнее проект, тем больше наворотов вы будете добавлять в свой код.

Всем удачи, надеюсь эта статья подтолкнет вас к написанию автотестов на Python, если вы еще этим не занялись!

Полезные ссылки