Tech Stack
Language
Framework
Reporting
Cicd
๐ Building a Robust API Test Automation Framework with Python
๐ฏ Introduction
Hey there, fellow automation enthusiasts! ๐ Ready to dive into something exciting? Today, I'm going to walk you through our super-cool API testing framework that I've built using Python. It's like having a Swiss Army knife for API testing - versatile, reliable, and surprisingly elegant!
๐ฎ What's in it for You?
Before we dive deep, here's what you're going to get:
- ๐๏ธ A rock-solid, maintainable testing framework
- ๐ Beautiful logging that actually makes sense
- ๐ Support for all your environments (staging, prod, UAT)
- โจ Super easy test case creation
- ๐ Test results that tell a story
๐ฐ Architecture Overview
Our architecture follows a modular design pattern that makes it highly maintainable, readable, and extensible. Think of our framework as a well-organized castle, where each component has its specific role:
Framework Layers Explained
-
Base API Layer ๐
- The foundation that handles all HTTP communications
- Implements wrappers for REST calls (GET, POST, PUT, DELETE)
- Manages request/response lifecycles and error handling
- Located in Core/base_api.py
-
API Player Layer ๐ฎ
- Orchestrates test actions and manages test state/context
- Interfaces directly with endpoint classes through composition
- Contains business logic and test-oriented wrappers
- Formats requests, processes responses, and handles errors
- Maintains session information and authentication
- Located in Core/api_player.py
-
Endpoints Layer ๐
- Abstracts the endpoints of the application under test
- One class per feature area (e.g., CarsAPIEndpoints, RegistrationAPIEndpoints)
- Maps directly to the application's API structure
- No business logic, only endpoint definitions and basic request formatting
- Located in Endpoints directory
-
Results Tracking Layer ๐
- Tracks test outcomes and provides detailed reporting
- Manages logging and result collection
- Helps generate meaningful test reports
- Located in Utils/results.py
๐ฎ Meet the Dream Team
1. ๐ฏ The Commander: API Player (Core/api_player.py)
Meet our superstar - the APIPlayer class! This is the heart of our framework and the layer most test cases interact with directly. It serves as an interface between test cases and the endpoint classes while maintaining test context/state.
class APIPlayer:
"""Your API testing commander-in-chief ๐ฎ"""
def __init__(self, base_url: str, results: Results):
self.results = results
self.logger = get_logger("APIPlayer")
# Create our endpoint specialists
self.cars_api = CarsAPIEndpoints(base_url)
self.auth_api = AuthAPIEndpoints(base_url)
self.users_api = UsersAPIEndpoints(base_url)
# Init session state
self.session_token = None
Key Responsibilities
- State Management ๐พ
- Maintains test context and session information
- Tracks test state across multiple API calls
- Stores authentication details and headers
2. ๐ The Scorekeeper: Results Tracking (Utils/results.py)
Meet our meticulous scorekeeper! This little genius keeps track of everything that happens in our tests:
class Results:
"""Your friendly neighborhood test tracker ๐"""
def __init__(self, log_file_path: Optional[str] = None):
self.total = 0
self.passed = 0
self.failed = 0
self.logger = get_logger("Results", log_file_path)
self.start_time = time.time()
3. ๐ The Specialists: Endpoint Classes
These specialists know exactly how to talk to each part of the API:
class CarsAPIEndpoints(BaseAPI):
"""Your gateway to the world of cars ๐"""
def __init__(self, base_url: str):
super().__init__(base_url)
self.logger = get_logger("CarsAPI")
def cars_url(self, suffix: str = '') -> str:
"""Build cars endpoint URL"""
return f"{self.base_url}/api/v1/cars{suffix}"
def get_cars(self, headers: dict = None) -> Response:
"""Get all cars from the kingdom"""
self.logger.info("Fetching all cars from the royal garage...")
return self.get(self.cars_url(), headers=headers)
Endpoint Class Features
-
Pure API Mapping ๐บ๏ธ
- Each method maps to exactly one API endpoint
- Methods handle URL construction and parameter formatting
- Simple 1:1 relationship with the API's structure
-
Clean Separation โ๏ธ
- No business logic in endpoint classes
- No test assertions or validations
- Focused solely on API communication
-
Composition Over Inheritance ๐งฒ
- Endpoint classes are composed by APIPlayer
- Easy to mix and match endpoints for different test scenarios
- New endpoints can be added without modifying existing code
4. ๐ The Foundation: BaseAPI
The rock-solid foundation everything is built on:
class BaseAPI:
"""The foundation of our API kingdom ๐ฐ"""
def __init__(self, base_url: str):
self.base_url = base_url
self.logger = get_logger("BaseAPI")
self.session = requests.Session()
# Setup connection pooling for better performance
adapter = HTTPAdapter(max_retries=3,
pool_connections=100,
pool_maxsize=100)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def __del__(self):
"""Close session when object is destroyed"""
if hasattr(self, 'session'):
self.session.close()
BaseAPI Features
-
Connection Efficiency ๐
- Uses connection pooling for better performance
- Implements retry strategy for transient failures
- Manages session lifecycle automatically
-
Error Handling ๐ก๏ธ
- Standardized error responses
- Common HTTP error handling
- Detailed logging for debugging
-
HTTP Methods ๐
- Implements all common HTTP methods (GET, POST, PUT, DELETE)
- Standardized parameter and header handling
- Consistent response processing
โจ Awesome Features
1. ๐ Environment Mastery
Our framework can juggle multiple environments like a pro:
# config/environments.py
environments = {
"dev": {
"base_url": "https://dev-api.example.com",
"username": "testuser",
"password": "devpassword123"
},
"staging": {
"base_url": "https://stg-api.example.com",
"username": "stguser",
"password": "stgpassword456"
},
"prod": {
"base_url": "https://api.example.com",
"username": "produser",
"password": "prod!password789"
}
}
Switching environments is as easy as:
pytest --env=staging
2. ๐ Beautiful Logs
Logs that tell a story, not just dump data:
self.logger.info("๐ Starting our epic car creation journey...")
self.logger.info("๐ก Sending our request to the Cars Kingdom")
self.logger.info(f"๐ Victory! Got response with status: {response.status_code}")
self.logger.error("๐จ Oops! Something went wrong with our request")
3. ๐ Victory Tracking
We celebrate every win and learn from every challenge:
def success(self, message: str) -> None:
"""Celebrate another victory! ๐"""
self.logger.info(f"๐ข VICTORY: {message}")
self.total += 1 # Another battle fought
self.passed += 1 # Another battle won
def failure(self, message: str, error=None) -> None:
"""Record the challenges we faced ๐"""
self.total += 1
self.failed += 1
if error:
self.logger.error(f"๐ด DEFEAT: {message} - {str(error)}")
else:
self.logger.error(f"๐ด DEFEAT: {message}")
4. ๐ Allure Reports Integration
Beautiful reports with Allure:
@allure.epic("Cars API")
@allure.feature("Car Management")
@allure.story("Creating Cars")
@allure.title("Add new car and verify it exists in the list")
@allure.description("Test to verify that the car count increases correctly after adding a new car. This ensures the system's counting mechanism works properly.")
@allure.tag("API", "Cars", "Validation")
@allure.severity(allure.severity_level.NORMAL)
def test_verify_car_count(api_player_fixture, auth_details, initial_car_count):
# Our epic test continues here...
Allure isn't just a tool - it's storytelling magic! ๐งโโ๏ธโจ
Screenshot from Allure Reports:
Run it easily with:
pytest --alluredir=./allure-results
allure serve allure-results # Open the magical portal to view reports
๐ Best Practices - Our Royal Decrees
-
๐ Clean Code is King
- ๐ Every class has its own castle (separation of concerns)
- ๐ Object-oriented design rules our kingdom
- ๐ Type hints light the way for future explorers
- ๐ PEP 8 style guide is our royal standard
-
๐ก๏ธ Error Handling is Sacred
- ๐ฏ Every API call is protected by try/except
- ๐จ Custom exceptions for clarity
- ๐ Detailed error messages
- ๐ฏ Status codes are always validated
-
๐ Test Results - Our Chronicles
- ๐ Automatic tracking of every quest
- ๐ Detailed reports of our adventures
- โฑ๏ธ Performance metrics for the speed demons
-
โ๏ธ Configuration - Our Master Plan
- ๐ Each environment gets its perfect setup
- ๐ฎ Easy controls through pytest options
- ๐ Logs that tell epic tales
๐ฎ Let's Write Some Epic Tests!
Let's see this framework in action with a real test example:
@pytest.fixture
def api_player_fixture():
"""Create our main character for this adventure"""
results = Results(log_file_path="./logs/test_log.txt")
player = APIPlayer(conf.base_url, results)
return player
def test_add_car(api_player_fixture, auth_details):
"""Add a car and verify it was added successfully"""
# GIVEN our API Player is ready for action
player = api_player_fixture
# AND we have a new car design blueprint
car_details = conf.car_details
# WHEN we add the car to the system
response = player.add_car(car_details, auth_details)
# THEN the API should confirm success
assert response.status_code == 201
assert response.json().get("success") == True
# AND when we get all cars
all_cars = player.get_cars(auth_details)
# THEN our new car should be in the list
cars_list = all_cars.json().get("cars", [])
new_car = next((car for car in cars_list if car["model"] == car_details["model"]), None)
assert new_car is not None
# Celebrate our victory! ๐
print("๐ Woohoo! Tesla has joined our awesome car collection!")
๐ค Cool Technical Stuff
1. ๐ต๏ธ Smart Error Detection
We've got your back with intelligent error handling:
def _execute_api_call(self, operation_name: str, api_call: Callable) -> Response:
"""Execute an API call with full error handling and logging"""
start_time = time.time()
try:
self.logger.info(f"๐ Starting {operation_name}")
response = api_call()
# Log performance metrics
execution_time = round((time.time() - start_time) * 1000, 2)
self.logger.info(f"โฑ๏ธ {operation_name} completed in {execution_time}ms")
# Validate response
if response.status_code < 400:
self.results.success(f"{operation_name} returned {response.status_code}")
else:
error_msg = f"{operation_name} failed with status {response.status_code}"
self.results.failure(error_msg)
self.logger.error(f"๐ Response body: {response.text}")
return response
except Exception as e:
# Handle unexpected errors
execution_time = round((time.time() - start_time) * 1000, 2)
error_msg = f"{operation_name} failed with exception: {str(e)}"
self.results.failure(error_msg, e)
self.logger.error(f"โฑ๏ธ Failed after {execution_time}ms")
raise APIPlayerException(error_msg) from e
2. ๐ Retry Mechanism
For those flaky API calls that need a second chance:
@retry(stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(ConnectionError))
def get(self, url: str, params: dict = None, headers: dict = None) -> Response:
"""Performs a GET request with retry logic for transient errors"""
self.logger.info(f"๐ GET: {url}")
return self.session.get(url, params=params, headers=headers, timeout=30)
3. ๐ฌ Test Breakdown
This test is like a thrilling adventure movie ๐ฌ in three exciting acts:
Act 1: Preparation ๐งโโ๏ธ
- Our hero,
api_player_fixture
, steps onto the stage (Thanks to pytest's fixture magic! โจ) - The sacred scroll of
auth_details
grants access to the kingdom's gates ๐ conf.car_details
reveals the blueprint for our new magical vehicle ๐
Act 2: The Quest ๐ฐ
- The
add_car
spell is cast, sending our request to the API realm ๐ก - We eagerly await the response scroll to see if our car materialized ๐ฌ
- The chronicles (logs) record every moment of our adventure for future bards ๐
Act 3: Verification ๐ต๏ธโโ๏ธ
- First checkpoint: Did the API respond with "success"? ๐
- Second mission: Let's gather ALL cars in the kingdom using
get_cars
๐๏ธ๐๏ธ๐๏ธ - Final challenge: Using the mystical
next()
function and a generator expression, we search for our newly created car in the royal collection ๐ - Victory dance if we find it! ๐
This test ensures our car creation spell works perfectly from start to finish, with no dragons ๐ or bugs ๐ getting in our way!
4. ๐งโโ๏ธ Lambda Magic: The Secret Sauce
Ever wonder why we use lambdas in our API calls? Here's the magical answer:
return self._execute_api_call(
"add_car",
lambda: self.cars_api.add_car(data=car_details, headers=headers)
)
This isn't just fancy code - it's wizardry! ๐งโโ๏ธโจ
- ๐ฐ๏ธ Lazy Execution: The lambda is like a spell waiting to be cast - it doesn't run until _execute_api_call decides it's time
- ๐ก๏ธ Error Shield: Our _execute_api_call function can wrap the API call in protective magic (try/except) to catch any fireballs that might come our way
- ๐ Metrics Mastery: We can measure how long spells take to cast and how much mana (resources) they consume
- ๐ Retry Rituals: If a spell fizzles, we can try casting it again without changing the original incantation
- ๐ Clean Scrolls: Our code stays neat and tidy, with all the messy error handling hidden away in one magical place
Without lambda magic, we'd need to repeat the same protective spells around every API call. Instead, we can focus on crafting the perfect API requests while our lambda takes care of the rest! ๐ฉโจ
๐ Coming Soon to Our Kingdom!
-
๐ฅ Async Powers
- ๐ Tornado-fast parallel testing
- ๐ Run tests at lightning speed
- ๐ Handle multiple requests like a boss
-
๐ Performance Wizardry
- ๐ Load testing that'll blow your mind
- โฑ๏ธ Response time tracking to the microsecond
- ๐ Beautiful performance dashboards
-
๐ Contract Testing Magic
- ๐ OpenAPI validation spells
- ๐งช Schema verification potions
- ๐ Ironclad contract enforcement
-
๐ Security Fortress
- ๐ก๏ธ Fort Knox-level authentication tests
- ๐ฎ Authorization checkpoints
- ๐ Security headers that even hackers respect
๐ The Grand Finale
There you have it, fellow adventurers! ๐ Our API testing framework is like a well-oiled machine (with a bit of magic sprinkled on top โจ). Here's what makes it awesome:
- ๐ Write tests that are fun and easy to read
- ๐ Track your victories with style
- ๐ Switch environments like a ninja
- ๐ Get reports that tell epic stories
- ๐ค Smart error handling that speaks human
Remember: Testing doesn't have to be boring! With our framework, every test is an adventure, every bug is a dragon to slay, and every passing test suite is a victory to celebrate! ๐