Operations 12 min read

How to Build a Scalable E‑Commerce QA Framework with Python, Pytest, and CI/CD

This guide details a modular e‑commerce QA framework that organizes configuration, HTTP clients, test data, fixtures, and test cases, integrates multi‑process execution with CI/CD pipelines, and defines quality gates to ensure reliable, maintainable automated testing for large‑scale services.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
How to Build a Scalable E‑Commerce QA Framework with Python, Pytest, and CI/CD

Overall Architecture

The framework follows a clear directory layout under ecommerce-qa-framework/, separating configuration, core libraries, test data, test cases, reports, scripts, documentation, and CI/CD definitions.

ecommerce-qa-framework/
├── config/            # configuration center
│   ├── __init__.py
│   ├── env/           # multi‑environment configs
│   │   ├── dev.yaml
│   │   ├── staging.yaml
│   │   └── prod.yaml
│   └── settings.py    # config loader
├── libs/              # core utilities
│   ├── __init__.py
│   ├── client/        # HTTP client
│   │   ├── base_client.py   # base wrapper with cookies/session handling
│   │   └── api_clients.py   # business‑domain clients (user, order, …)
│   ├── db/            # database helpers
│   ├── mq/            # optional message‑queue listener
│   ├── utils/        # data generator, assert helper, file reader
│   └── logger.py     # unified logging
├── data/              # test data (account pool, product data)
├── tests/             # test cases
│   ├── __init__.py
│   ├── conftest.py   # global fixtures
│   ├── common/        # shared steps (login, checkout, …)
│   ├── modules/       # business‑module tests (user, product, cart, …)
│   └── scenarios/     # end‑to‑end scenarios
├── reports/           # CI‑generated reports
├── scripts/           # operational scripts (run_tests.py, cleanup_test_data.py)
├── docs/              # documentation
├── requirements.txt
├── pytest.ini
├── .env.example
└── Jenkinsfile        # CI/CD pipeline

Core Modules

1. Configuration Management – Multi‑Environment Isolation

Configuration files live in config/env/. Example staging.yaml defines API base URL, database connection, and Redis settings. The ConfigLoader class loads the appropriate file based on the TEST_ENV environment variable, resolves placeholders like ${DB_PASSWORD}, and provides a convenient config.get("db.host") accessor.

# config/settings.py
import os, yaml
from pathlib import Path

class ConfigLoader:
    def __init__(self):
        self.env = os.getenv("TEST_ENV", "staging")
        config_path = Path(__file__).parent / "env" / f"{self.env}.yaml"
        with open(config_path) as f:
            self._config = yaml.safe_load(f)
        self._resolve_env_vars(self._config)

    def _resolve_env_vars(self, obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                obj[k] = self._resolve_env_vars(v)
        elif isinstance(obj, str) and obj.startswith("${") and obj.endswith("}"):
            key = obj[2:-1]
            return os.getenv(key, obj)
        return obj

    def get(self, key, default=None):
        from functools import reduce
        try:
            return reduce(dict.get, key.split("."), self._config)
        except Exception:
            return default

config = ConfigLoader()

2. HTTP Client – Automatic Cookie & Thread‑Safe Sessions

The base client creates a per‑service requests.Session with retry logic and stores it in a thread‑local variable, ensuring cookies are not shared across concurrent threads.

# libs/client/base_client.py
import threading, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from config.settings import config
from libs.logger import logger

class BaseAPIClient:
    _local = threading.local()
    def __init__(self, service_name="default"):
        self.service_name = service_name
        self.base_url = config.get(f"services.{service_name}.base_url") or config.get("base_url")
        self.timeout = config.get("http.timeout", 10)
    @property
    def session(self):
        if not hasattr(self._local, "sessions"):
            self._local.sessions = {}
        if self.service_name not in self._local.sessions:
            s = requests.Session()
            retry = Retry(total=3, backoff_factor=1, status_forcelist=[429,500,502,503,504])
            adapter = HTTPAdapter(max_retries=retry)
            s.mount("http://", adapter)
            s.mount("https://", adapter)
            self._local.sessions[self.service_name] = s
        return self._local.sessions[self.service_name]
    def request(self, method, endpoint, **kwargs):
        url = f"{self.base_url}{endpoint}"
        logger.info(f"[{self.service_name}] → {method.upper()} {url}")
        resp = self.session.request(method, url, timeout=self.timeout, **kwargs)
        logger.info(f"[{self.service_name}] ← {resp.status_code} ({resp.elapsed.total_seconds():.2f}s)")
        return resp

3. Test Data Management – Account & Product Pools

CSV files under data/accounts/ store user credentials. Helper functions generate order numbers and fetch random users.

# libs/utils/data_generator.py
import random, string
from datetime import datetime

def generate_order_no(prefix="ORD"):
    now = datetime.now().strftime("%Y%m%d%H%M%S")
    rand = "".join(random.choices(string.digits, k=6))
    return f"{prefix}{now}{rand}"

def get_random_user(role="normal"):
    import csv
    path = f"data/accounts/{role}_users.csv"
    with open(path, encoding="utf-8") as f:
        users = list(csv.DictReader(f))
    return random.choice(users)

4. Global Fixture – Multi‑User Concurrency

In tests/conftest.py, a session‑scoped fixture provides a unique test user per test class, leveraging the data generator and the UserClient to log in before yielding the user object.

# tests/conftest.py
import pytest
from libs.client.api_clients import UserClient
from libs.utils.data_generator import get_random_user

@pytest.fixture(scope="session")
def worker_id(request):
    """Get xdist worker ID for unique account allocation"""
    return request.config.workerinput.get("workerid", "master")

@pytest.fixture(scope="class")
def test_user(worker_id):
    """Assign an isolated user to each test class"""
    user = get_random_user()
    client = UserClient()
    resp = client.login(user["username"], user["password"])
    assert resp.status_code == 200
    yield user
    # optional teardown: logout or clean cart

5. Test Case Writing Guidelines

Tests are organized per business module. Example for order creation shows data preparation, API calls, and JSONPath assertions via a shared assert_helper.

# tests/modules/order/test_create_order.py
import pytest
from libs.client.api_clients import OrderClient, ProductClient
from libs.utils.assert_helper import assert_equal, assert_in_response

class TestCreateOrder:
    """Order creation – happy path"""
    def test_create_order_success(self, test_user):
        product = ProductClient().get_product(1001)
        assert product.status_code == 200
        order_payload = {
            "items": [{"product_id": 1001, "quantity": 1}],
            "address_id": 101,
            "order_no": generate_order_no()
        }
        resp = OrderClient().create_order(order_payload)
        assert_equal(resp.status_code, 200)
        assert_in_response(resp, "$.data.order_no", str)

6. Multi‑Threaded Execution & CI Integration

The scripts/run_tests.py script reads environment variables to configure pytest parallelism, markers, and the target environment, then launches pytest with HTML reporting. The Jenkinsfile defines a four‑stage pipeline (checkout, install, run tests, archive report) that publishes the HTML report after each build.

# scripts/run_tests.py
import os, sys, subprocess

def main():
    env = os.getenv("TEST_ENV", "staging")
    workers = os.getenv("PYTEST_WORKERS", "4")
    markers = os.getenv("PYTEST_MARKERS", "smoke")
    cmd = [
        "pytest",
        f"-n {workers}",
        f"-m {markers}",
        "--html=reports/report.html",
        "--self-contained-html",
        "--tb=short",
        f"--env={env}"
    ]
    result = subprocess.run(" ".join(cmd), shell=True)
    sys.exit(result.returncode)

if __name__ == "__main__":
    main()

Team Collaboration Mechanism

A checklist ensures each test case covers core paths, uses the test_user fixture to avoid account conflicts, applies unified assertions, and tags tests with @pytest.mark.smoke or @pytest.mark.regression for selective execution.

Quality Gates

Pre‑test: All smoke cases (core flow) must pass.

Pre‑release: Regression suite pass rate ≥ 95%.

Daily build: Full suite runs automatically; failures must be fixed within 24 hours.

Data cleanup: cleanup_test_data.py runs after each execution to prevent dirty data accumulation.

Conclusion – Why This Framework Fits the Team

Standardization: Unified client, assert, data, and logging layers reduce collaboration friction.

Extensibility: Adding new business domains only requires subclassing BaseAPIClient.

High Concurrency: Built‑in support for xdist multi‑process execution scales regression runs.

Maintainability: Clear separation of configuration, data, and logic enables new members to onboard within a week.

Engineering Flow: From local development to CI/CD, the entire lifecycle is automated.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

e-commercePythonci/cdpytest
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.