API Testing: Playwright and Python (Part 2)

Abhilash Sharma
4 min readMay 30, 2023

--

In this part, we will start with developing the test automation framework.

⭐️ Overview

  1. Move request context to conftest file
  2. Reusable template for endpoints
  3. Request processing library
  4. HTTP Methods
  5. Config Parser
  6. Logger Config
  7. Write tests Pythonistas way (Part 3) 😃

🔗 Link to Part 1

Conftest File -> conftest.py

Here we will add a request_context fixture with a session scope so that this can be reused during the entire testing regression suite.

import pytest
from typing import Generator
from playwright.sync_api import Playwright, APIRequestContext
from core.utils.config_parser import get_config


@pytest.fixture(scope="session")
def request_context(playwright: Playwright) -> \
Generator[APIRequestContext, None, None]:
"""
This is request context fixture to be reused for request processing.
:param playwright: instance for Playwright library
:return:it returns the request context
"""
r_context = playwright.request.new_context(
base_url=get_config("BaseConfig", "base_url")
)
yield r_context
r_context.dispose()

Base Endpoint -> base_endpoint.py

The base endpoint will act as an interface for different endpoint modeling which will be used for request processing further.

"""
This will act like a template for further request processing.
"""
from abc import ABC, abstractmethod


class IEndpointTemplate(ABC):
@abstractmethod
def url(self) -> str:
"""
This function is used for fetching the endpoint.
:return: it will return the endpoint string.
"""
pass

@abstractmethod
def http_method(self) -> str:
"""
This method is used for fetching the http method to process.
:return: it returns the http method name.
"""
pass

@abstractmethod
def query_parameters(self) -> dict | None:
"""
This method is used to pass the query parameters.
:return: it returns the parameter dictionary.
"""
pass

@abstractmethod
def path_parameters(self, **kwargs) -> dict | None:
"""
This method is used to pass the path parameters.
:param kwargs: here we can pass the path parameter values
which can further passed to endpoint formats
:return: it returns the dictionary of path parameter values
"""
pass

@abstractmethod
def headers(self) -> dict:
"""
This method is used to pass the request headers.
:return: it returns the dictionary of request headers.
"""
pass

@abstractmethod
def request_body(self) -> dict | None:
"""
This method is used to pass the request body.
:return: it returns the dictionary of request body.
"""
pass

Request Processor -> base_client.py

"""
This module is used for basic CRUD operations using Playwright -> APIRequestContext
"""
from playwright.sync_api import APIRequestContext
from core.base.base_endpoint import IEndpointTemplate
from core.constants.http_methods import HttpMethods


class BaseClient:

def __init__(self, request_context: APIRequestContext):
self.request_context = request_context

def request_processor(self, endpoint: IEndpointTemplate.__class__, **kwargs) -> (int, dict):
"""
This function processes the http request based on http methods
:param endpoint: it takes endpoint specifications which can be
provided by extending the "IEndpointTemplate" interface
:param kwargs: it takes keyword arguments required in special cases,
these are optional arguments
:return: it returns http status code and response
"""
url = endpoint.url()
http_method = endpoint.http_method()
query_params = endpoint.query_parameters()
path_params = endpoint.path_parameters(**kwargs)
headers = endpoint.headers()
request_body = endpoint.request_body()

if path_params:
for key, value in path_params.items():
url = url.replace(f'{{{key}}}', str(value))

if query_params:
url += '?'
url += '&'.join([f'{key}={value}' for key, value in query_params.items()])

response = None

match http_method:
case HttpMethods.GET.name:
response = self.request_context.get(url=url, headers=headers)
case HttpMethods.POST.name:
response = self.request_context.post(url=url, headers=headers, data=request_body)

return response.status, response.json()

HTTP Methods -> http_methods.py

Here we will create the enum for HTTP methods.

from enum import Enum, auto


class HttpMethods(Enum):
"""
These are the enums values for http methods.
"""
POST = auto()
GET = auto()
PUT = auto()
DELETE = auto()

Config Parser -> config_parser.py

This parser will be used to fetch and set the config values.

import configparser
import os


cur_path = os.path.abspath(os.path.dirname(__file__))
config_file = os.path.join(cur_path, r"../../config.ini")


def get_config(section, key) -> str:
"""
This method is used to fetch the config values for config file.
:param section: here we pass the config section value
:param key: here we pass the key value
:return: it returns the value based on the section & variable
"""
config = configparser.ConfigParser()
config.read(config_file)
return config.get(section=section, option=key)


def get_endpoint(key) -> str:
"""
This method is used to fetch the different endpoints from config file
:param key: here we pass the key parameter value
:return: it returns the endpoint string
"""
return get_config("EndPoints", key)


def set_config(section, key, value):
"""
This method is used to set the config values in config file.
:param section: here we pass the config section value
:param key: here we pass the key
:param value: here we pass the actual value to be set for the key
:return: None
"""
config = configparser.ConfigParser()
config.read(config_file)
config.set(section=section, option=key, value=value)
with open(config_file, "w") as configfile:
config.write(configfile)

📔 config.ini

[BaseConfig]
base_url = https://reqres.in

[EndPoints]
create_user_endpoint = /api/users
get_user_endpoint = /api/users/{user_id}

Logger Config -> logger_config.py

This logger config will be used to set the logging configuration for different modules.

import logging
import logging.config
import os


cur_path = os.path.abspath(os.path.dirname(__file__))
logger_config_file = os.path.join(cur_path, r"../../logger_config.ini")


def get_logger(module_name: str) -> logging:
"""
This method is used for setting the logger module.
:param module_name: here we pass the module name
where logger is being used.
:return:
"""
logging.config.fileConfig(fname=logger_config_file, disable_existing_loggers=False)
return logging.getLogger(module_name)

📔 logger_config.ini

[loggers]
keys=root,sampleLogger

[handlers]
keys=consoleHandler

[formatters]
keys=sampleFormatter

[logger_root]
level=INFO
handlers=consoleHandler

[logger_sampleLogger]
level=INFO
handlers=consoleHandler
qualname=sampleLogger
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=sampleFormatter
args=(sys.stdout,)

[formatter_sampleFormatter]
format="%(asctime)s — %(name)s — %(levelname)s — %(funcName)s:%(lineno)d — %(message)s"

In the next part, we will discuss more about how to use these core libraries to write maintainable test scripts.

Stay tuned.

Thanks

--

--

No responses yet