Automating API Testing with Python

Good day! In this article I am going to continue the story about my small experience of automation. In the last article I showed how to do it using Postman – today I will show how to implement it using the Python programming language, the Pytest framework, and the Requests library.

First, let me introduce the project tree.

Project tree

Project tree

Next about auxiliary files: helpers.py, configKey.py.

In helpers.py, three project tokens are generated: the main one and two integrating services, and the response scheme validation method is used.

import pytest
from jsonschema import validate as validate_json, ValidationError
import requests as r
from configKey import *
import time

today = time.strftime("%Y-%m-%d", time.localtime())


def validate_schema(response, schema):
    try:
        validate_json(response, schema)
    except ValidationError as e:
        print(e)
        return False
    return True


def update_headers():
    """Первичная проверка на доступ сервера"""
    try:
        response = r.post(**_url, json=**_person_data, verify=False)
        **_headers['Authorization'] = response.text

        response = r.get(url=***_url, headers=***_headers, verify=False)
        ***_headers['Authorization'] = 'Bearer ' + response.json()['****']

        response = r.get(url=****_url, headers=**_headers, verify=False)
        ****_headers['Authorization'] = 'Bearer ' + response.json()['*****']
    except:
        return False
    return True

The principle of token generation is as follows – a request is sent to the server, the token is extracted directly from the response, it is entered into a separate variable and reused in tests when importing helpers.py. The most important thing is two imports: Pytest, requests – the first for running tests, the second for using the library that will send requests.

configKey contains the main base URLs and header schemes.

Directly to the tests. EMIAS service. The following imports must be performed.

import pytest
import requests as r
from configKey import *
from schemas.schemas_valid import *
from helpers import *

Next, we enter a class that will contain variables, methods, and tests. The most important thing is that the class name should begin with “Test”.

class Test***:
    """Классовые переменные с динамическими данными"""
    visit_id_home=""  """Айди вызова врача на дом"""
    visit_id = ''       """Айди записи к врачу"""
    available_time="" """Талон к врачу"""

    @classmethod
    def setup_class(cls):
        """Принудительное обновление токенов"""
        if not update_headers():
            pytest.exit('Ошибка получения токенов', 3) 

    @classmethod
    def teardown_class(cls):
        """Удаление данных"""
        update_headers()
        if Test***.visit_id_home:
            r.delete(f'{link}/*******/{Test***.visit_id_home}', headers=***_headers)
        if Test***.visit_id:
            r.delete(f'{link}/*****/{Test***.visit_id}', headers=***_headers)

On the 7th and 13th lines I introduce class methods: setup_class(cls), teardown_class(cls) – so that when any test in this class is launched, these methods are always called. setup_class(cls) – update tokens that are generated in helpers.py before launch, teardown_class(cls) – delete data after launch. P.s.: it is good manners to clear the space after yourself from test data to avoid duplicates or inability to register, since there can only be one active record – 400 “Bad request” errors will fall.

I described the user flow in the previous article, you can read it there) – this article is an addition to the previous one.

Directly to the tests. The test names must start with “test” so that pytest can identify them. Don't forget to observe tabulation – our tests are inside the class, so class methods and tests are one Tab to the right.

Before the test, I use two methods to parse response bodies.

 @classmethod
    def parse_doctor_data(cls, response):
        items = response.json()['items'][0]
        for doctor in items['doctors']:
            for sched in doctor['schedule']:
                if sched['count_tickets'] > 0:
                    print(sched['date'])
                    return {
                        'available_date' : sched['date'][0:10],
                        'doctor_id' : doctor['id'],
                        'lpu_code' : doctor['lpu_code'],
                    }
        return {}

  @staticmethod
  def parse_ticket_time_data(response):
      schedule = response.json()['schedule']
      for sched in schedule:
          if not sched['busy']:
              return sched['time']
      return ''

On the 3rd line, I enter the variable items, where I am interested in the first open day for an appointment. On the 4th line, I start a loop that goes through each available doctor. Then, on the 5th line, I start a nested loop, in which I want to get all the schedules of free days for an appointment. On the 6th line, I specify whether there are any free tickets, if all conditions are met, then I ask to write the data to the global environment variables. A nested loop is used because the response body array consists of several layers.

    def test_visit_to_doctor(self):
        """Получение свободных талонов к врачу"""
        url = f'{link}/*********'
        response = r.get(url, headers=***_headers)
        assert response.status_code == 200, f'Ожидался статус код 200 ОК, но получен {response.status_code}'
        assert validate_schema(response.text, test_visit_to_doctor_1), 'Json schema does not match'
        doctor_data = TestEmias.parse_doctor_data(response)

        """Получение талонов к врачу по конкретному дню."""
        assert doctor_data['available_date'] and doctor_data['doctor_id'], "Данные о враче не были установлены."

        url = f"{link}/*****/{doctor_data['doctor_id']}?*****={doctor_data['available_date']}"
        response = r.get(url, headers=***_headers)
        assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'
        assert validate_schema(response.text, test_visit_to_doctor_2), 'Json schema does not match'
        Test***.available_time = Test***.parse_ticket_time_data(response)

        """Запись на прием к врачу."""
        assert TestEmias.available_time, "Время талона не было установлено."
        url = f'{link}/*****'
        payload = {
            "doctor_id": doctor_data['doctor_id'],
            "email": "***@yandex.ru",
            "lpu_code": doctor_data['lpu_code'],
            "day": doctor_data['available_date'],
            "time": Test***.available_time
        }
        response = r.post(url=url, headers=***_headers, json=payload)
        assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'
        assert validate_schema(response.text, test_visit_to_doctor_3), 'Json schema does not match'
        Test***.visit_id = response.json()['entry_id']

        """Отмена записи к врачу"""
        assert Test***.visit_id, 'Не высталвлен visit_id'
        response = r.delete(url=url + f'/{Test***.visit_id}', headers=***_headers)
        assert response.status_code == 204, f'Ожидался статус-код ОК, но получен {response.status_code}'
        assert validate_schema(response.text, test_visit_to_doctor_4), 'Json schema does not match'
        Test***.visit_id = ''

In order to send a request as part of a test we need:

1) Designate the test, which is done in the first line;

2) Enter the required request parameters: url, headers, body (post, put, patch – requests).

2.1) URL – specify the address, in my example I call {link}, because this is the base URL, which is in helpers.py, and under the asterisks are the endpoints, if you specify a direct link, it will look something like this: url=”https://habr.com/ru/article/new/”.

2.2) Headers – since my headers are pulled from helpers.py, I import them, so I don’t enter them.

2.3*) If you have a Post, Put or Patch request, then add the line 'payload' or name it your own.

3) Sending a request – since I imported the “Response as r” library my requests look like this (three required arguments if the request is Post):

response = r.post(url=url, headers=***_headers, json=payload)

4) Check – this can be a check for a status code, for the presence of a specific key in the response, for the response time, or a check of the scheme – checks are used individually, but we almost always check for a status code.

 assert response.status_code == 200, f'Ожидался статус код ОК, но получен {response.status_code}'

The assert construct works like this (for those who don't know): if the 200 status code is returned, then everything is ok, and if another status code is returned, for example, 403, then the assert is triggered and returns you an error code – based on the second part of the upper check "{response.status_code}'{response.status_code}".

5*) Print out the answer or part of it. For example: print(response.text)

You can print a URL, a new variable or a request body for debugging, but usually pdb (python debug) is used for this, but you can also do it directly in the tests, but locally)

6*) In my case, I need to write the response body to a class variable for later use.

Test***.visit_id = response.json()['entry_id']

Under the stars is the class name, visit_id is the name of the new variable (don't forget to specify it in advance at the beginning of the class) = a specific key from the answer will be written to the variable.

6*) Validation schemes. Discuss with the team whether you need such checks. In a separate file “schemas_valid.py”. They look something like this:

test_doctor_visit_home_schema_3 = {
        "code": 200,
        "message": "Вызов отменен",
        "message_code": "2000"
}

test_banners_schema = {
    "required": [
        "content",
        "pageable",
        "totalPages",
        "totalElements",
        "last",
        "number",
        "size",
        "sort",
        "numberOfElements",
        "first",
        "empty"
    ]
}

test_sessionId_schema = {
  "sessionId": str,
  "required": ["sessionId"]
}

7) Launch. I recommend launching from the IDE terminal or system terminal using the following command: pytest -s -v -k "название теста"

Or run the entire file.

Right-click on the file and copy the relative path

Right-click on the file and copy the relative path

We write: Pytest + paste the relative path from the clipboard + “-s -v”

pytest tests/smoke_test/high/test_003.py -s -v

Sample test answers

Sample test answers

8) Make sure that the test files are in the “test” folder and the names of the files, classes and tests start with 'test'. Check that 'requirements.txt' contains the following dependencies:

jsonschema==4.22.0
pytest==7.4.4
Requests==2.32.3

To avoid unnecessary libraries from bothering you, delete the file 'requirements.txt' IN THIS PROJECT and run the command: pip3 install pipreqs

Thank you for reading to the end, I hope this article will help you)

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *