Architecture
Support for additional technologies, e.g. ElasticSearch, can be added by sub-classing these classes and adding specific steps, setup/teardown, and configuration. This allows reusing the basic configuration, reporting, logging, and retrying mechanisms. Further, application tests, steps, and configurations reuse by subclassing from technologies.
flowchart TD
A[Tests: Define BDD scenarios as series of steps, also define specific setup and teardown] --> |contains| B[Steps: encapsulate UI or API operations and verifications, and may be composed of other steps]
B --> |contains| C[Configurations: can be per environment, such as dev, qa, staging, and contain URLs, users, authentication schemes, encryption, etc.]
B --> |uses| D[Matchers: Hamcrest matchers for single objects or for iterables]
A --> |contains| C
B --> |uses| E[Models: domain objects]
subgraph Inheritance
A1[GenericTests] -.-> |inherits| A2[Tests]
B1[GenericSteps] -.-> |inherits| B2[Steps]
C1[AbstractConfiguration] -.-> |inherits| C2[Configuration]
end
Extending the Framework
To add support for a new technology (e.g., messaging, database), create: -
MyTechConfiguration(BaseConfiguration)
-MyTechSteps(GenericSteps[MyTechConfiguration])
-MyTechTests(AbstractTestsBase[MyTechSteps, MyTechConfiguration])
This pattern ensures you reuse the core BDD, configuration, and reporting mechanisms.
classDiagram
%% Core Abstractions
class AbstractTestsBase {
<>
+steps
+_configuration
+setup_method()
+teardown_method()
}
class GenericSteps {
<>
+given
+when
+then
+and_
+with_
+retrying()
+eventually_assert_that()
}
class BaseConfiguration {
<>
+parser
}
%% Technology-Specific Extensions
class RestTests
class RestSteps
class RestConfiguration
class SeleniumTests
class SeleniumSteps
class SeleniumConfiguration
%% Example: Custom Extension
class TerminalXTests
class TerminalXSteps
class TerminalXConfiguration
%% Relationships
AbstractTestsBase <|-- RestTests
AbstractTestsBase <|-- SeleniumTests
SeleniumTests <|-- TerminalXTests
GenericSteps <|-- RestSteps
GenericSteps <|-- SeleniumSteps
SeleniumSteps <|-- TerminalXSteps
BaseConfiguration <|-- RestConfiguration
BaseConfiguration <|-- SeleniumConfiguration
SeleniumConfiguration <|-- TerminalXConfiguration
RestTests o-- RestSteps : uses
RestTests o-- RestConfiguration : configures
SeleniumTests o-- SeleniumSteps : uses
SeleniumTests o-- SeleniumConfiguration : configures
TerminalXTests o-- TerminalXSteps : uses
TerminalXTests o-- TerminalXConfiguration : configures
%% Example extension note
%% You can add new technologies by subclassing the three core abstractions:
%% AbstractTestsBase, GenericSteps, and BaseConfiguration.
Key Classes
Class | Description |
---|---|
AbstractTestsBase |
Base for all test scenarios; holds steps and config |
GenericSteps |
Base for all step implementations; provides BDD keywords |
BaseConfiguration |
Base for all configuration objects |
RestTests |
REST-specific test base |
RestSteps |
REST-specific steps |
RestConfiguration |
REST-specific configuration |
SeleniumTests |
Selenium-specific test base |
SeleniumSteps |
Selenium-specific steps |
SeleniumConfiguration |
Selenium-specific configuration |
TerminalXSteps |
Example: custom UI steps |
TerminalXConfiguration |
Example: custom UI configuration |
Usage Examples
TerminalX Tests
@pytest.mark.external
@pytest.mark.selenium
class TerminalXTests(
SeleniumTests[TerminalXSteps[TerminalXConfiguration],
TerminalXConfiguration]):
_steps_type = TerminalXSteps
_configuration = TerminalXConfiguration()
# NOTE sections may be further collected in superclasses and reused across tests
def login_section(
self, user: TerminalXUser) -> TerminalXSteps[TerminalXConfiguration]:
return (self.steps
.given.terminalx(self.web_driver)
.when.logging_in_with(user.credentials)
.then.the_user_logged_in(is_(user.name)))
def should_login(self):
self.login_section(self.configuration.random_user)
def should_find(self):
(self.login_section(self.configuration.random_user)
.when.clicking_search())
for word in ["hello", "kitty"]:
(self.steps
.when.searching_for(word)
.then.the_search_hints(yields_item(tracing(
contains_string_ignoring_case(word)))))
@override
def setup_method(self) -> None:
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.firefox import GeckoDriverManager
if self._configuration.parser.has_option("selenium", "browser_type") \
and self._configuration.parser["selenium"]["browser_type"] == "firefox":
options = FirefoxOptions()
service = FirefoxService(GeckoDriverManager().install())
self._web_driver = Firefox(options=options, service=service)
self._web_driver.set_window_size(1920, 1080) # type: ignore
else:
super().setup_method()
The Setup Method
The setup_method
demonstrates how default setup behavior can be overriden.
In real world it would be pulled into a superclass that extends SeleniumTests
.
The Configuration
Furthermore, the self._configuration.parser["selenium"]["browser_type"]
could
be defined as a method on the TerminalXConfiguration
class, or a superclass of
it.
The configuration is loaded from two sources, in this example:
TerminalXConfiguration
class looks for a matchingterminalx_configuration.ini
file underconfigurations/
.- pytest could be launched with a
--config
parameter to override this or add properties:pytest --config selenium:browser_type=firefox qa-pytest-examples/tests/terminalx_tests.py::TerminalXTests
Any subclass of BaseConfiguration
looks for a matching ini
file, this way multiple configurations can be used.
If there is a TEST_ENVIRONMENT
environment variable its value will be chained
to the path of ini
file, this way one can select which configuration set
shall be used at runtime.
Swagger Petstore Tests
@pytest.mark.external
class SwaggerPetstoreTests(
RestTests[SwaggerPetstoreSteps[SwaggerPetstoreConfiguration],
SwaggerPetstoreConfiguration]):
_steps_type = SwaggerPetstoreSteps
_configuration = SwaggerPetstoreConfiguration()
@pytest.mark.parametrize("pet", SwaggerPetstorePet.random(range(4)))
def should_add(self, pet: SwaggerPetstorePet):
(self.steps
.given.swagger_petstore(self.rest_session)
.when.adding(pet)
.then.the_available_pets(yields_item(tracing(is_(pet)))))
Combined Tests
@pytest.mark.external
@pytest.mark.selenium
class CombinedTests(
RestTests[CombinedSteps, CombinedConfiguration],
SeleniumTests[CombinedSteps, CombinedConfiguration]):
_steps_type = CombinedSteps
_configuration = CombinedConfiguration()
def should_run_combined_tests(self):
random_pet = next(SwaggerPetstorePet.random())
random_user = random.choice(self.configuration.users)
(self.steps
.given.swagger_petstore(self.rest_session)
.when.adding(random_pet)
.then.the_available_pets(yields_item(tracing(is_(random_pet))))
.given.terminalx(self.web_driver)
.when.logging_in_with(random_user.credentials)
.then.the_user_logged_in(is_(random_user.name)))
RabbitMQ Self Tests
class RabbitMqSelfTests(
RabbitMqTests[int, str,
RabbitMqSteps[int, str, RabbitMqSelfConfiguration],
RabbitMqSelfConfiguration]):
_queue_handler: QueueHandler[int, str]
_steps_type = RabbitMqSteps
_configuration = RabbitMqSelfConfiguration()
def should_publish_and_consume(self) -> None:
(self.steps
.given.a_queue_handler(self._queue_handler)
.when.publishing([Message("test_queue")])
.and_.consuming()
.then.the_received_messages(yields_item(
tracing(is_(Message("test_queue"))))))
@override
def setup_method(self) -> None:
super().setup_method()
self._queue_handler = QueueHandler(
channel := self._connection.channel(),
queue_name=require_not_none(
channel.queue_declare(
queue=EMPTY_STRING, exclusive=True).method.queue),
indexing_by=lambda message: hash(message.content),
consuming_by=lambda bytes: bytes.decode(),
publishing_by=lambda string: string.encode())
@override
def teardown_method(self) -> None:
try:
self._queue_handler.close()
finally:
super().teardown_method()
qa_testing_utils.pytest_plugin
QA Testing Utils – Pytest Plugin
This pytest plugin provides shared testing infrastructure for Python monorepos
or standalone projects using the qa-testing-utils
module.
Features
- Per-module logging configuration:
- During test session startup, the plugin searches for a
logging.ini
file:- First under
tests/**/logging.ini
- Then under
src/**/logging.ini
- Falls back to
qa-testing-utils/src/qa_testing_utils/logging.ini
- First under
-
This enables consistent, per-module logging without requiring repeated boilerplate.
-
Source code inclusion in test reports:
-
Adds a
body
section to each test report with the source code of the test function (viainspect.getsource()
), useful for HTML/Allure/custom reporting. -
Command-line config overrides (parsed but not yet consumed):
- Adds a
--config
option that acceptssection:key=value,...
strings. - Intended for runtime configuration injection (e.g., overriding .ini files or test settings).
Usage
-
Declare the plugin in your module's
pytest_plugins
(if not auto-loaded via PDM entry point): pytest_plugins = ["qa_testing_utils.pytest_plugin"] -
Place a logging.ini file under your module's tests/ or src/ directory. If none is found, the fallback from qa-testing-utils will be used.
-
Run your tests, optionally with runtime overrides:
pytest --config my_section:key1=val1,key2=val2
Notes: This plugin is designed to be generic and reusable across any module consuming qa-testing-utils.
Compatible with VSCode gutter test launching and monorepo test execution.
get_config_overrides()
Returns parsed --config
overrides passed to pytest.
Safe to call from anywhere (e.g., BaseConfiguration).
Source code in qa-testing-utils/src/qa_testing_utils/pytest_plugin.py
113 114 115 116 117 118 |
|
pytest_addoption(parser)
Adds the --config
command-line option for runtime config overrides.
Source code in qa-testing-utils/src/qa_testing_utils/pytest_plugin.py
61 62 63 64 65 66 67 68 |
|
pytest_configure(config)
Configures the pytest session, loading logging.ini and parsing config overrides.
Source code in qa-testing-utils/src/qa_testing_utils/pytest_plugin.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
|
pytest_runtest_makereport(item, call)
Generates a test report with the source code of the test function.
Source code in qa-testing-utils/src/qa_testing_utils/pytest_plugin.py
100 101 102 103 104 105 106 107 108 109 110 |
|