QA Engineer Skills 2026QA-2026Functional Patterns for Testing

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

  1. Write a closure-based API client factory that supports different auth methods (token, API key, basic auth)
  2. Write a test data factory that generates unique users, orders, and products
  3. Process a list of API responses using functional pipeline: filter errors, extract error messages, group by error code, generate a summary report
  4. 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)