QA Test Framework: Architecture¶
This document provides a deep dive into the technical architecture of the QA test framework. It is intended for developers who want to understand the core design, patterns, and advanced concepts that underpin the test suite.
For practical instructions on setup, writing tests, and daily workflow, please see the Developer Guide.
Table of Contents¶
- Framework Overview - A high-level look at the architecture.
- Directory Structure - What's where in the
qa/directory. - Architectural Patterns - Key design patterns used in the framework.
- Advanced Topics - Deeper architectural details for contributors.
Framework Overview¶
Component Layers¶
flowchart TD
subgraph Features["Feature Files Layer - Gherkin BDD"]
F1[access_test.feature]
F2[ftmo_eval_global.feature]
F3[oanda_prop_trading.feature]
end
subgraph Steps["Step Definitions Layer"]
S1[authentication_steps.py]
S2[document_steps.py]
S3[verification_steps.py]
end
subgraph Business["Business Logic Layer"]
B1[ImportConfig]
B2[DocumentBase]
B3[TranslationHandler]
end
subgraph Helpers["Helper Layer"]
H1[Money]
H2[ExchangeRates]
H3[DateHelper]
H4[GCSHelper]
end
subgraph Models["Model Layer"]
M1[Invoice<br/>50+ Helios fields]
M2[Organization]
M3[DocumentMetadata]
end
subgraph Data["Data Layer"]
D1[Enums<br/>Currency, DatabaseSetting]
D2[Test Data<br/>Payment info]
D3[Test Users<br/>50+ users]
end
subgraph External["External Systems"]
E1[Helios<br/>Czech]
E2[NetSuite<br/>US]
E3[Xero<br/>Australia]
E4[GCS<br/>Oanda]
end
Features --> Steps
Steps --> Business
Business --> Helpers
Helpers --> Models
Models --> Data
Data --> External
Test Execution Flow¶
flowchart TD
A[1. Behave starts] --> B[2. environment.py - before_all<br/>Load config, setup logging,<br/>load users, initialize controllers]
B --> C[3. Feature file parsed]
C --> D[4. authentication_steps.py<br/>Set import_user & user,<br/>create controllers]
D --> E[5. document_steps.py<br/>Parse parameters -- ImportConfig<br/>Build import request 50+ fields<br/>Call _import_document_with_retry]
E --> F{6. Retry Logic<br/>max 3 attempts}
F -->|201| G[Success]
F -->|409| H[Regenerate timestamp<br/>Retry]
F -->|429| I[Exponential backoff<br/>Retry]
H --> F
I --> F
G --> J[7. verification_steps.py<br/>Assert status code]
J --> K[8. document_steps.py validation<br/>GET document from API<br/>Create AccountingDocumentBase<br/>deepdiff_compare]
K --> L[9. Test Result<br/>Pass/Fail]
Framework Principles¶
- Separation of Concerns - Clear boundaries between layers
- Single Responsibility - Each component has one job
- DRY (Don't Repeat Yourself) - Shared logic in helpers
- Testability - All components are testable independently
- Extensibility - Easy to add new platforms, countries, item types
- Type Safety - Pydantic models provide runtime validation
- Immutability - Prefer immutable data structures where possible
Directory Structure¶
graph TD
A["qa/"] --> B["config.py<br/>Configuration management"]
A --> C["environment.py<br/>Behave hooks"]
A --> D["logger.py<br/>Logging setup"]
A --> E["makefile<br/>Build automation"]
A --> F["pyproject.toml<br/>Dependencies"]
A --> G["DEVELOPER_GUIDE.md<br/>Daily guide"]
A --> H["ARCHITECTURE.md<br/>Technical reference"]
A --> I["enums/<br/>Constants"]
I --> I1["enums_currency.py<br/>21 currencies + rates"]
I --> I2["enums_db_setting.py<br/>Database → Platform"]
I --> I3["enums_vat_rate.py<br/>VAT rates by country"]
A --> J["features/<br/>Gherkin scenarios"]
J --> J1["access_test.feature"]
J --> J2["ftmo_eval_global.feature"]
A --> K["helpers/<br/>Utilities"]
K --> K1["money.py<br/>Currency operations"]
K --> K2["exchange_rates.py<br/>Static rates"]
K --> K3["document_base.py<br/>Document validation"]
A --> L["model/<br/>Pydantic models"]
L --> L1["import_config.py<br/>Test configuration"]
L --> L2["document_import.py<br/>Invoice 50+ fields"]
L --> L3["organization_data.py<br/>Customer data"]
A --> M["steps/<br/>Step definitions"]
M --> M1["authentication_steps.py<br/>Auth setup"]
M --> M2["document_steps.py<br/>Document ops"]
M --> M3["verification_steps.py<br/>Assertions"]
A --> N["test_data/<br/>Test data"]
N --> N1["README.md<br/>Data guide"]
N --> N2["test_users.dev.yaml<br/>50+ users"]
N --> N3["payment_info_*.json<br/>Payment data"]
Module Index¶
Configuration¶
- config.py - Settings management
- environment.py - Behave hooks (setup/teardown)
- logger.py - Logging configuration
Enums¶
- enums_currency.py - 21 currencies with exchange rates
- enums_db_setting.py - Database → Platform mapping
- enums_company.py - FTMO entities (IDs 5, 6)
- enums_vat_rate.py - VAT rates by country
- enums_item_types.py - Invoice item types
- enums_regions.py - Countries, states, tax regimes
- enums_bank_account.py - Bank account details
- enums_labels.py - UI labels and translations
- enums_ar_translations.py - Arabic translations
Helpers¶
- money.py - Currency operations (Money class)
- exchange_rates.py - Static exchange rates (Singleton)
- date_helper.py - Date/timezone operations
- document_base.py - Document preparation/validation
- document_helper.py - Utility functions
- gcs_helper.py - Google Cloud Storage operations
- file_helper.py - Test data loading
- transformation_helper.py - Data transformation utilities
- translation_handler.py - Multi-language support
Models¶
- base_model.py - Base models (MixedCase, KebabCase)
- import_config.py - Test configuration
- document_import.py - Invoice (50+ Helios fields)
- document_item.py - Parsed invoice items
- document_metadata.py - Payment info (22 fields)
- organization_data.py - Organization/customer data
- document_html.py - HTML response models
- document_json.py - JSON response models
- responses.py - Response model exports
Steps¶
- init.py - Package overview and step index
- authentication_steps.py - Token setup (2 steps)
- document_steps.py - Document operations (15+ steps)
- verification_steps.py - Status code validation (1 step)
Architectural Patterns¶
1. Layered Architecture¶
The framework follows a strict layered architecture:
┌─────────────────────────────────────────────────┐
│ Presentation Layer (BDD) │
│ - Feature files (.feature) │
│ - Gherkin scenarios │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Step Definition Layer │
│ - Step implementations │
│ - Context management │
│ - Orchestration logic │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Business Logic Layer │
│ - ImportConfig (document builder) │
│ - AccountingDocumentBase (expectations) │
│ - Helper functions │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Model Layer │
│ - Pydantic models │
│ - Enumerations │
│ - Data validation │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Service Layer │
│ - HTTP clients (Controller) │
│ - Authentication (AuthController) │
│ - External integrations │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ External Systems │
│ - Accounting Service API │
│ - Keycloak │
│ - Database (indirect) │
└─────────────────────────────────────────────────┘
Layer Responsibilities:
- Presentation: Human-readable test scenarios
- Step Definitions: Glue between Gherkin and code
- Business Logic: Core test logic and document manipulation
- Model: Data structures and validation
- Service: External communication
- External Systems: APIs and third-party services
2. Factory Pattern¶
Purpose: Create objects without specifying exact class
Implementation:
class TranslationFactory:
"""Factory for creating translation models based on country"""
@classmethod
def get_translation_model(cls, vat_country: str) -> Type[BaseModel] | None:
"""
Returns appropriate translation model for country.
Args:
vat_country: ISO country code (AE, SA, OM)
Returns:
Translation model class or None
"""
match vat_country:
case "AE" | "SA" | "OM":
return ArabicInvoiceModel
case _:
return None
@classmethod
def get_provider(cls, country_code: str) -> TranslationProvider:
"""Get translation provider for specific country"""
return cls._providers.get(country_code, NoTranslationProvider())
Benefits:
- Decouples client code from concrete classes
- Easy to add new translation models
- Centralized object creation logic
3. Builder Pattern¶
Purpose: Construct complex objects step by step
Implementation: ImportConfig class
class ImportConfig:
"""Builder for document import requests"""
def __init__(self, init_data: dict):
"""Initialize with table data from feature file"""
self._init = ImportConfigInitModel(**init_data)
self._setup_derived_attributes()
def build_import_document(self, user_data: User) -> dict:
"""
Build complete import document structure.
Returns:
Complete document JSON ready for API
"""
self.document_body = {
"init": self._init_db_data,
"invoice": [],
"organization": self._organization_data,
}
# Build main invoice item
main_item = self._create_invoice_item()
# Add metadata if present
if self._init.payment_info:
main_item["Metadata"] = self._payment_info
self.document_body["invoice"].append(main_item)
# Add additional items (points, discounts, etc.)
self._add_additional_items()
return self.document_body
Benefits:
- Complex construction logic isolated
- Step-by-step building with validation
- Reusable for different document types
4. Strategy Pattern¶
Purpose: Define family of algorithms, encapsulate each one
Implementation: Platform-specific handling
class ItemType(Enum):
"""Item type with platform-specific strategies"""
def account_number(self, db_setting: DatabaseSetting) -> str:
"""Get account number based on platform strategy"""
match db_setting.platform:
case Platform.HELIOS:
return self._helios_account_number()
case Platform.XERO:
return self._xero_account_number()
case Platform.NETSUITE:
return self._netsuite_account_number()
case _:
raise NotImplementedError(f"Platform {db_setting.platform}")
def _helios_account_number(self) -> str:
"""Helios-specific account number logic"""
return CisloUcetPol.get_helios_account_number(self)
def _xero_account_number(self) -> str:
"""Xero-specific account number logic"""
return self.value.xero_type
def _netsuite_account_number(self) -> str:
"""NetSuite-specific account number logic"""
return self.value.code
Benefits:
- Platform-specific logic encapsulated
- Easy to add new platforms
- Runtime platform selection
5. Adapter Pattern¶
Purpose: Convert interface to expected interface
Implementation: MixedCaseModel
class MixedCaseModel(BaseModel):
"""
Adapter for Pydantic models with PascalCase aliases.
Converts Python snake_case to JSON PascalCase.
"""
model_config = ConfigDict(
alias_generator=to_pascal, # Convert field names
populate_by_name=True, # Accept both formats
str_strip_whitespace=True, # Clean strings
validate_assignment=True, # Validate on change
)
def compare(self, other: "MixedCaseModel", **kwargs) -> dict:
"""Compare two models using DeepDiff"""
return DeepDiff(
self.model_dump(by_alias=True),
other.model_dump(by_alias=True),
**kwargs
)
Benefits:
- Bridges Python conventions with API requirements
- Automatic case conversion
- Consistent interface across models
6. Repository Pattern¶
Purpose: Encapsulate data access logic
Implementation: AuthController as user repository
class AuthController:
"""Repository for user data and authentication"""
def __init__(self, users: dict, kc_base_url: str, default_client: str):
self.users = users # User repository
self._kc_base_url = kc_base_url
self._default_client = default_client
self._token_cache: dict[str, Token] = {}
def get_token(self, user_id: str, client_id: str) -> Token:
"""
Retrieve token from cache or generate new one.
Repository pattern: Abstracts token storage/retrieval.
"""
cache_key = f"{user_id}:{client_id}"
if cache_key in self._token_cache:
token = self._token_cache[cache_key]
if not token.is_expired():
return token
# Generate new token
token = self._fetch_token_from_keycloak(user_id, client_id)
self._token_cache[cache_key] = token
return token
Benefits:
- Centralized data access
- Caching strategy encapsulated
- Easy to change data source
Advanced Topics¶
This section covers advanced architectural concepts, design decisions, and strategies for extending the framework.
Core Components¶
Data & Service Layers¶
Strategy & Design¶
1. Context Management¶
File: environment.py
Purpose: Behave hooks for test lifecycle management
def before_all(context: Context) -> None:
"""
Initialize global test context before any tests run.
Responsibilities:
- Load configuration
- Initialize logging
- Create service controllers
- Setup authentication
"""
context.settings = AccountingServiceTestConfig()
configure_logging(context.settings.log_level)
auth_controller = AuthController(
users=context.settings.users,
kc_base_url=StaticConfig.KEYCLOAK_BASE_URL,
default_client=context.settings.users.default_client,
)
context.controller = Controller(
auth_controller=auth_controller,
base_url=context.settings.base_url,
)
def before_feature(context: Context, feature: Feature) -> None:
"""Initialize feature-level context"""
context.users = context.controller.auth.users
def before_scenario(context: Context, scenario: Scenario) -> None:
"""Initialize scenario-level context"""
context.sent_import_data = None
context.import_config = None
context.imported_documents = []
context.gcs_blobs_to_delete = []
def after_scenario(context: Context, scenario: Scenario) -> None:
"""Cleanup after scenario"""
for bucket_name, blob_name in getattr(context, "gcs_blobs_to_delete", []):
delete_gcs_blob(bucket_name, blob_name)
Context Lifecycle:
- before_all - Global setup (once per test run)
- before_feature - Feature setup (per .feature file)
- before_scenario - Scenario setup (per test)
- [Test execution]
- after_scenario - Scenario cleanup
- after_feature - Feature cleanup (if defined)
- after_all - Global cleanup (if defined)
2. Configuration Management¶
File: config.py
Architecture:
class StaticConfig:
"""Static configuration from environment variables"""
KEYCLOAK_BASE_URL = os.getenv("KEYCLOAK_BASE_URL")
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
@classmethod
def validate(cls) -> None:
"""Validate all required config is present"""
required = ["KEYCLOAK_BASE_URL"]
missing = [k for k in required if not getattr(cls, k)]
if missing:
raise ConfigurationError(f"Missing: {missing}")
class AccountingServiceTestConfig:
"""Runtime configuration with validation"""
def __init__(self):
self.base_url = os.getenv("BASE_URL")
self.timeout = int(os.getenv("TIMEOUT", "30"))
self.log_level = os.getenv("LOG_LEVEL", "INFO")
# Load users from secrets manager or config
self.users = self._load_users()
# Validate configuration
self._validate()
def _validate(self) -> None:
"""Validate configuration is complete"""
if not self.base_url:
raise ConfigurationError("BASE_URL not set")
Configuration Layers:
- Environment Variables - Override everything
- Config Files - Default values
- Secrets Manager - Sensitive data (if integrated)
- Hard-coded Defaults - Fallback values
3. Logger Architecture¶
File: logger.py
Design:
def configure_logging(log_level: str = "INFO") -> None:
"""
Configure logging with custom format and handlers.
Logger hierarchy:
- root: Catches all
- steps_logger: Step definitions
- http_logger: HTTP requests
- validation_logger: Validation
"""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/test_execution.log'),
logging.StreamHandler(sys.stdout)
]
)
# Configure specific loggers
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('requests').setLevel(logging.WARNING)
# Specialized loggers
steps_logger = logging.getLogger('steps')
http_logger = logging.getLogger('http')
validation_logger = logging.getLogger('validation')
Data Flow Architecture¶
Document Import Flow¶
sequenceDiagram
participant F as Feature File
participant S as Step Definition
participant IC as ImportConfig
participant C as Controller
participant API as Backend API
participant V as Validator
F ->> S: When I POST document data
S ->> IC: new ImportConfig(table_data)
IC ->> IC: Parse & validate input
IC ->> IC: Resolve enums
IC ->> IC: Load user data
S ->> IC: build_import_document(user)
IC ->> IC: Create init section
IC ->> IC: Create invoice items
IC ->> IC: Add metadata
IC ->> IC: Create organization data
IC -->> S: document_body
S ->> C: import_document(body)
C ->> C: Add authentication
C ->> API: POST /import/document/id
API -->> C: 201 {id: "01118753"}
C -->> S: response
S ->> S: Store document_id in context
F ->> S: When I GET html
S ->> C: get_html_document(cid, doc_id)
C ->> API: GET /document/{id}/html
API -->> C: HTML response
C -->> S: response
S ->> V: parse HTML
V ->> V: Extract values
V -->> S: html_dict
S ->> IC: AccountingDocumentBase(config)
IC ->> IC: Calculate expected values
IC -->> S: expected_data
S ->> V: InvoiceModel.from_html(dict)
V -->> S: received_data
S ->> V: deepdiff_compare(expected, received)
V -->> S: differences (or empty)
S -->> F: Test result
Validation Flow¶
flowchart TD
A[Response Received] --> B{Response Type?}
B -->|HTML| C[BeautifulSoup Parse]
B -->|JSON| D[JSON Parse]
C --> E[Extract Values to Dict]
D --> F[Load to Pydantic Model]
E --> G[InvoiceModel.from_html]
F --> H[JsonDocumentResponse model]
G --> I[Received Model]
H --> I
J[ImportConfig] --> K[AccountingDocumentBase]
K --> L[Calculate Expected Values]
L --> M{Document Type?}
M -->|HTML| N[InvoiceModel.from_document_base]
M -->|JSON| O[JsonDocumentResponse.from_document_base]
N --> P[Expected Model]
O --> P
I --> Q[model_dump]
P --> R[model_dump]
Q --> S[Round Floats]
R --> S
S --> T[DeepDiff Compare]
T --> U{Differences?}
U -->|Yes| V[Assert Fail with Details]
U -->|No| W[Test Pass]
style V fill: #ff6b6b
style W fill: #51cf66
Model Layer Architecture¶
Pydantic Model Hierarchy¶
BaseModel (Pydantic)
│
├── MixedCaseModel (Custom base)
│ ├── ImportConfigInitModel
│ ├── Invoice
│ ├── JsonDocument
│ ├── JsonDocumentItem
│ ├── JsonDocumentResponse
│ ├── OandaJsonDocumentResponse
│ └── InvoiceModel
│
├── Item (Document item representation)
├── Organization (Organization data)
└── User (User data)
Model Design Principles¶
1. Separation by Purpose:
- Import Models (
document_import.py) - API request structures - Response Models (
document_json.py,document_html.py) - API responses - Business Models (
import_config.py,document_base.py) - Business logic - Enum Models (
enums/) - Configuration and constants
2. Factory Methods:
class InvoiceModel(MixedCaseModel):
"""HTML document validation model"""
@classmethod
def from_html(cls, html_dict: dict, document_body: dict) -> Self:
"""Create from parsed HTML"""
return cls(
document_id=html_dict.get("document-id"),
invoice_value=html_dict.get("invoice-number"),
# ... extract and transform fields
)
@classmethod
def from_document_base(cls, document_base: AccountingDocumentBase) -> Self:
"""Create from expected values"""
payment_info = MetadataModel.build_expected_data(document_base)
organization_info = OrganizationInfoModel.build_expected_data(document_base)
invoice_dto = InvoiceModel.build_expected_data(document_base)
return cls(**{
**invoice_dto.model_dump(),
**payment_info.model_dump(),
**organization_info.model_dump()
})
3. Validation at Boundaries:
class JsonDocumentResponse(MixedCaseModel):
"""Response model with validation"""
id: str = Field(min_length=8, max_length=12)
document: JsonDocument
documentItems: list[JsonDocumentItem] = Field(min_length=1)
model_config = ConfigDict(
str_strip_whitespace=True, # Clean strings
validate_assignment=True, # Validate on change
extra='forbid' # Reject unknown fields
)
@field_validator('id')
@classmethod
def validate_document_id(cls, v: str) -> str:
"""Custom validation for document ID format"""
if not v.startswith(('01', '02', '90', '80', '70')):
raise ValueError(f"Invalid document ID prefix: {v}")
return v
Service Layer Design¶
HTTP Client Architecture¶
class Controller:
"""
Main HTTP client for accounting service.
Architecture:
- Handles authentication via AuthController
- Provides high-level API methods
- Manages request/response lifecycle
- Implements retry logic
"""
def __init__(self, auth_controller: AuthController, base_url: str):
self.auth = auth_controller
self.base_url = base_url
self.timeout = 30
self.session = requests.Session()
def __getitem__(self, key: tuple[str, str]) -> "Controller":
"""
Indexer pattern for setting auth context.
Usage:
controller = context.controller["CLIENT_NBO", "service"]
controller.import_document(body)
"""
user_id, client_id = key
self._current_user = user_id
self._current_client = client_id
return self
def _get_headers(self) -> dict:
"""Build headers with authentication"""
token = self.auth.get_token(self._current_user, self._current_client)
return {
"Authorization": f"Bearer {token.access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
def _request(
self,
method: str,
endpoint: str,
**kwargs
) -> requests.Response:
"""
Internal request method with error handling.
Handles:
- Timeout
- Connection errors
- HTTP errors
- Logging
"""
url = f"{self.base_url}{endpoint}"
try:
http_logger.debug(f"{method} {url}")
response = self.session.request(
method=method,
url=url,
headers=self._get_headers(),
timeout=self.timeout,
**kwargs
)
http_logger.debug(f"Response: {response.status_code}")
return response
except requests.Timeout:
http_logger.error(f"Timeout after {self.timeout}s")
raise
except requests.ConnectionError as e:
http_logger.error(f"Connection error: {e}")
raise
def import_document(self, body: dict) -> requests.Response:
"""Import document to accounting system"""
return self._request(
"POST",
"/v1/service/import/document/id",
json=body
)
def get_html_document(self, cid: int, document_id: str) -> requests.Response:
"""Retrieve HTML document"""
return self._request(
"GET",
f"/v1/user/{cid}/document/{document_id}/html"
)
Authentication Management¶
class AuthController:
"""
Manages authentication tokens with caching.
Features:
- Token caching
- Automatic refresh
- Multiple client support
"""
def __init__(self, users: dict, kc_base_url: str, default_client: str):
self.users = users
self._kc_base_url = kc_base_url
self._default_client = default_client
self._token_cache: dict[str, Token] = {}
self._cache_lock = threading.Lock()
def get_token(self, user_id: str, client_id: str) -> Token:
"""
Get token with thread-safe caching.
Flow:
1. Check cache
2. Validate expiry
3. Return cached or fetch new
"""
cache_key = f"{user_id}:{client_id}"
with self._cache_lock:
if cache_key in self._token_cache:
token = self._token_cache[cache_key]
if not token.is_expired():
return token
# Fetch new token
token = self._fetch_token_from_keycloak(user_id, client_id)
self._token_cache[cache_key] = token
return token
def _fetch_token_from_keycloak(
self,
user_id: str,
client_id: str
) -> Token:
"""Fetch token from Keycloak"""
user = self.users[user_id]
response = requests.post(
f"{self._kc_base_url}/realms/{self._realm}/protocol/openid-connect/token",
data={
"grant_type": "password",
"client_id": client_id,
"username": user.username,
"password": user.password,
}
)
if response.status_code != 200:
raise AuthenticationError(f"Failed to get token: {response.text}")
token_data = response.json()
return Token(
access_token=token_data["access_token"],
expires_in=token_data["expires_in"],
refresh_token=token_data.get("refresh_token"),
fetched_at=datetime.now()
)
Testing Strategy¶
Test Pyramid¶
┌─────────────┐
│ E2E Tests │ (Feature files)
│ (Slow) │ Full flow validation
└─────────────┘
▲
┌────────────────┐
│ Integration │ (Step definitions + HTTP)
│ Tests (Medium) │ API interaction
└────────────────┘
▲
┌────────────────────┐
│ Unit Tests │ (Helpers, Models)
│ (Fast) │ Business logic
└────────────────────┘
Test Organization¶
By Type:
- Regression Tests - Full feature coverage
- Integration Tests - Multi-component scenarios
- Unit Tests - Individual component testing
By Feature:
- Document Import - Invoice/credit note import
- Document Retrieval - HTML/JSON retrieval
- Validation - Data validation
- Access Control - GDPR and permissions
- Multi-Platform - Helios, Xero, NetSuite, Oanda
Test Data Strategy¶
1. Fixture Files:
test_data/
├── payment_info_special_tip_a.json
├── payment_info_special_tip_b.json
├── payment_info_points.json
└── payment_info_challenge.json
2. Enum-Based Data:
class Company(Enum):
"""Company data as enum"""
FTMO_EVAL_GLOBAL = CompanyField(
item_id=5,
full_name="FTMO Evaluation Global s.r.o.",
address="Purkyňova 2121/3",
# ... all company data
)
3. Factory Functions:
def create_test_user(
user_id: str,
database: DatabaseSetting,
country: Country = Country.CZ
) -> User:
"""Factory for creating test users"""
return User(
cid=generate_cid(),
username=f"test_{user_id}",
database=database,
country=country,
)
Extension Points¶
Adding New Platform¶
1. Define Platform:
# enums/enums_db_setting.py
class Platform(Enum):
HELIOS = auto()
XERO = auto()
NETSUITE = auto()
NEW_PLATFORM = auto() # Add new platform
2. Implement Strategy:
# enums/enums_item_types.py
class ItemType(Enum):
def item_id(self, db_setting: DatabaseSetting) -> str:
match db_setting.platform:
case Platform.HELIOS:
return self.value.helios_type
case Platform.XERO:
return self.value.xero_type
case Platform.NETSUITE:
return self.value.code
case Platform.NEW_PLATFORM:
return self.value.new_platform_type # Add mapping
3. Add Configuration:
# enums/enums_db_setting.py
class DatabaseSetting(Enum):
NEW_DB = DatabaseConfig(
db_name="NewPlatformDB",
platform=Platform.NEW_PLATFORM,
company=Company.NEW_COMPANY,
# ... config
)
Performance Considerations¶
1. Lazy Evaluation¶
Use @cached_property for expensive computations:
@cached_property
def total_vat_czk_money(self) -> Money:
"""Expensive calculation - cached"""
if not self.show_czk:
return Money(0, Currency.CZK)
# Complex calculation
czk_amount = self._calculate_vat_in_czk()
return Money(czk_amount, Currency.CZK)
2. Connection Pooling¶
Use requests.Session() for connection reuse:
class Controller:
def __init__(self, ...):
self.session = requests.Session() # Connection pool
self.session.mount('https://', HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=3
))
3. Parallel Test Execution¶
Use Behave's parallel execution:
Considerations:
- Thread-safe context management
- Isolated test data
- No shared state between scenarios
Security Architecture¶
1. Credential Management¶
Never hardcode credentials:
# ❌ Wrong
password = "mypassword123"
# ✅ Correct
password = os.getenv("USER_PASSWORD")
# ✅ Better - use secrets manager
password = secrets_manager.get_secret("qa/user/password")
2. Token Security¶
Token handling:
class Token:
"""Secure token with expiry"""
def __init__(self, access_token: str, expires_in: int, ...):
self._access_token = access_token # Private
self._expires_at = datetime.now() + timedelta(seconds=expires_in)
@property
def access_token(self) -> str:
"""Read-only access to token"""
if self.is_expired():
raise TokenExpiredError("Token has expired")
return self._access_token
def is_expired(self) -> bool:
"""Check if token is expired (with buffer)"""
buffer = timedelta(seconds=30)
return datetime.now() >= (self._expires_at - buffer)
def __repr__(self) -> str:
"""Don't expose token in repr"""
return f"Token(expires_at={self._expires_at})"
3. Sensitive Data Handling¶
Masking in logs:
def log_request(url: str, headers: dict, body: dict):
"""Log request with sensitive data masked"""
safe_headers = {
k: "***REDACTED***" if k.lower() in ["authorization", "api-key"] else v
for k, v in headers.items()
}
safe_body = mask_sensitive_fields(body, ["password", "secret", "token"])
http_logger.debug(f"Request: {url}")
http_logger.debug(f"Headers: {safe_headers}")
http_logger.debug(f"Body: {json.dumps(safe_body)}")
Design Decisions¶
Why Static Exchange Rates?¶
Decision: Use static rates instead of live API
Rationale: - Test predictability (same input → same output) - No external dependencies - Fast test execution - Easy debugging with known rates
Trade-off: Rates don't match live markets, but tests verify logic not market data
Why Dual-Token Authentication?¶
Decision: Separate import_user and user contexts
Rationale: - Test service-level imports (CLIENT_NBO) - Test user-level access control - Mirrors production pattern - Enables cross-user tests
Implementation: Two controller instances per test
Why Pydantic Over Dataclasses?¶
Decision: Use Pydantic for all models
Rationale: - Runtime validation - Type safety - Serialization built-in - Better error messages - IDE autocompletion
Trade-off: Slightly more overhead, but worth it for safety
Why Behave Over pytest-bdd?¶
Decision: Use Behave for BDD
Rationale: - More mature Gherkin parser - Better feature file organization - Flexible hooks system - Comprehensive documentation
Trade-off: pytest-bdd better integrates with pytest, but Behave is sufficient