Functional Patterns for Testing
Functional programming patterns complement OOP in test automation. While OOP structures your framework (page objects, test classes), functional patterns process data: filtering test results, transforming API responses, building flexible test utilities with closures and factories.
Map, Filter, and Reduce
These three operations form the core of functional data processing. They replace explicit loops with declarative transformations.
Python
# map: transform each item
responses = [requests.get(f"{base_url}/users/{i}") for i in range(1, 11)]
status_codes = list(map(lambda r: r.status_code, responses))
# [200, 200, 404, 200, 500, ...]
# filter: keep items matching a condition
errors = list(filter(lambda r: r.status_code >= 500, responses))
assert len(errors) == 0, f"Server errors found: {[r.url for r in errors]}"
# reduce: aggregate into a single value
from functools import reduce
total_time = reduce(lambda acc, r: acc + r.elapsed.total_seconds(), responses, 0)
print(f"Total request time: {total_time:.2f}s")
# Pythonic alternatives (often preferred)
status_codes = [r.status_code for r in responses] # list comprehension > map
errors = [r for r in responses if r.status_code >= 500] # comprehension > filter
total_time = sum(r.elapsed.total_seconds() for r in responses) # sum > reduce
JavaScript/TypeScript
const responses = await Promise.all(
Array.from({ length: 10 }, (_, i) => fetch(`${baseUrl}/users/${i + 1}`))
);
// map
const codes = responses.map(r => r.status);
// [200, 200, 404, 200, 500, ...]
// filter
const errors = responses.filter(r => r.status >= 500);
expect(errors).toHaveLength(0);
// reduce
const totalTime = responses.reduce((acc, r) => acc + r.duration, 0);
// Chaining: filter, then map, then reduce
const errorMessages = responses
.filter(r => r.status >= 400)
.map(r => `${r.status}: ${r.url}`)
.join("\n");
Closures
A closure captures variables from its enclosing scope — useful for factory functions that create pre-configured utilities.
API Client Factory
def make_api_client(base_url: str, token: str):
"""Creates a pre-configured API client."""
def request(method: str, path: str, **kwargs):
headers = {"Authorization": f"Bearer {token}"}
headers.update(kwargs.pop("headers", {}))
return requests.request(method, f"{base_url}{path}", headers=headers, **kwargs)
return request
# Usage: create clients for different environments
staging_api = make_api_client("https://api.staging.example.com", os.environ["STAGING_TOKEN"])
prod_api = make_api_client("https://api.example.com", os.environ["PROD_TOKEN"])
# Both use the same interface
staging_response = staging_api("GET", "/users/me")
prod_response = prod_api("GET", "/users/me")
Retry Wrapper
def with_retry(max_attempts: int = 3, delay: float = 1.0):
"""Creates a retry wrapper for flaky operations."""
def decorator(func):
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt < max_attempts - 1:
time.sleep(delay * (attempt + 1))
raise last_exception
return wrapper
return decorator
@with_retry(max_attempts=3, delay=0.5)
def fetch_user(user_id: str):
response = requests.get(f"{base_url}/users/{user_id}")
response.raise_for_status()
return response.json()
TypeScript Closures
function makeApiClient(baseUrl: string, token: string) {
return async (method: string, path: string, body?: object) => {
const response = await fetch(`${baseUrl}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
return response;
};
}
const api = makeApiClient("https://api.staging.example.com", process.env.TOKEN!);
const response = await api("GET", "/users/me");
Higher-Order Functions
A higher-order function takes a function as an argument or returns a function. They are the building blocks of flexible test utilities.
Custom Assertion Builder
def assert_response(response, status=None, contains=None, excludes=None):
"""Flexible response assertion."""
if status is not None:
assert response.status_code == status, \
f"Expected {status}, got {response.status_code}: {response.text[:200]}"
if contains is not None:
body = response.json()
for key in contains:
assert key in body, f"Response missing field: {key}"
if excludes is not None:
body = response.json()
for key in excludes:
assert key not in body, f"Response contains forbidden field: {key}"
# Usage
assert_response(r, status=200, contains=["id", "name"], excludes=["password"])
Test Data Generator
def make_user_factory(defaults: dict):
"""Creates a factory that generates user data with overrides."""
counter = 0
def create(**overrides):
nonlocal counter
counter += 1
user = {**defaults, **overrides}
user["email"] = user.get("email", f"testuser{counter}@test.com")
return user
return create
create_user = make_user_factory({"role": "viewer", "active": True})
# Each call creates a unique user with defaults
user1 = create_user() # testuser1@test.com, viewer, active
user2 = create_user(role="admin") # testuser2@test.com, admin, active
user3 = create_user(email="custom@test.com") # custom@test.com, viewer, active
Immutability
Functional programming favors immutable data — data that is not modified after creation. This prevents subtle bugs where shared test data is accidentally modified.
# BAD: mutable default — all tests share the same list
def create_order(items=[]): # list is created once and shared!
items.append("new_item")
return items
# GOOD: immutable approach
def create_order(items=None):
items = list(items or []) # create a new list each time
items.append("new_item")
return items
# BEST: use frozen dataclass for test data
from dataclasses import dataclass
@dataclass(frozen=True)
class TestUser:
email: str
password: str
role: str = "viewer"
admin = TestUser("admin@test.com", "pass123", "admin")
# admin.role = "viewer" # This raises FrozenInstanceError
Pipe/Chain Pattern
Process data through a series of transformations, each step's output feeding the next step's input.
# Process test results through a pipeline
def load_results(path):
with open(path) as f:
return json.load(f)
def filter_failures(results):
return [r for r in results if r["status"] == "FAIL"]
def enrich_with_duration(results):
return [{**r, "slow": r["duration"] > 5.0} for r in results]
def format_report(results):
return "\n".join(f"FAIL: {r['name']} ({r['duration']}s)" for r in results)
# Pipeline
results = load_results("results.json")
report = format_report(enrich_with_duration(filter_failures(results)))
// TypeScript: chaining with array methods
const report = testResults
.filter(r => r.status === "FAIL")
.map(r => ({ ...r, slow: r.duration > 5.0 }))
.map(r => `FAIL: ${r.name} (${r.duration}s)${r.slow ? " [SLOW]" : ""}`)
.join("\n");
When to Use Functional vs OOP
| Situation | Approach | Why |
|---|---|---|
| Page objects, framework structure | OOP | State management, encapsulation |
| Data transformation in assertions | Functional | map/filter/reduce are concise |
| Pre-configured utilities | Closures | Capture configuration, return focused functions |
| Test data generation | Factories (closure) | Unique data with consistent defaults |
| Processing test results | Functional pipeline | Each step is independent and testable |
| Complex test setup | OOP (fixtures) | State lifecycle management |
Practical Exercise
- Write a closure-based API client factory that supports different auth methods (token, API key, basic auth)
- Write a test data factory that generates unique users, orders, and products
- Process a list of API responses using functional pipeline: filter errors, extract error messages, group by error code, generate a summary report
- Refactor an existing test that uses a for-loop into a functional style using map/filter
Key Takeaways
- map/filter/reduce replace explicit loops with declarative data processing
- Closures create pre-configured functions — essential for API clients and factories
- Higher-order functions build flexible, reusable test utilities
- Favor immutable data in test setup to prevent shared-state bugs
- Combine functional patterns (data processing) with OOP (framework structure)