API Testing: Playwright and Python (Part 2)
In this part, we will start with developing the test automation framework.
⭐️ Overview
- Move request context to conftest file
- Reusable template for endpoints
- Request processing library
- HTTP Methods
- Config Parser
- Logger Config
- Write tests Pythonistas way (Part 3) 😃
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