In the past couple of posts, I’ve written about using markers and fixtures to help you write better tests. So now your project has a high test coverage, and you’ve caught bugs. To investigate, you want to access specific tests with specific parameters. And perhaps you also want to run tests with different parameters without changing your code. So, how do you structure your test to achieve this?
Let’s illustrate the solution with an example. You want to create a test with four parameters, A, B, C, and D. Additionally, you have the following requirements:
- Have default values for each parameter,
- Specify values for a subset of the parameters if desired (such as via the command line)
- Use
pytest.mark.parametrize()
to parametrize a subset of parameters
Let’s see how we can achieve this.
1. Adding Custom CLI Options to Pytest
You can extend Pytest’s default functionality by adding custom command-line options using the pytest_addoption
hook. This is particularly useful for customizing tests dynamically without modifying the code.
# contest.py
import pytest
# Add custom CLI options
def pytest_addoption(parser):
parser.addoption("--A", action="store", default=None, help="stores the given value as string")
parser.addoption("--B", action="store_const", default=0, const=1, help="stores constant value")
parser.addoption("--C", action="store_true", help="if given, stores True. Otherwise, stores False.")
parser.addoption("--D", action="store_false", help="if given, stores False. Otherwise, stores True.")
parser.addoption("--ALL", action="store_true", help="if given, runs all tests")
pytest_addoption
is a special “hook function” by Pytest’s hook-based plugin system. If hook functions are defined in a test suite, Pytest will discover them and automatically call them during its initialization. This hook must be place in conftest.py
(docs).
The parser
object is part of the pytest_addoption
hook. The parser.addoption()
method adds new arguments to pytest’s CLI. The syntax used here is equivalent to argparse
library’s add_argument() function. For example, each argument can have:
- A name (e.g., –A, –B).
- An action (e.g., store saves the argument’s value). Unfortunately I honestly could not find documentation that has a list of all the options available.
- A default value used when the argument is not provided.
- A help message displayed in pytest –help.
Note that store_const
is an action type in argparse (used by pytest) that always assigns a predefined constant value (specified by const) to the option when it is used.
I’ll provide a full example in a later section.
2. Accessing the CLI option values
The request
object is a built-in pytest fixture that provides information about the test context and configuration. It allows access to command-line options, test parameters, and other runtime data. For example, request.config.getoption("--OPTION_NAME")
retrieves the value of a custom command-line option defined via parser.addoption()
.
For example:
@pytest.fixture
def non_parametrized_inputs(request):
C = request.config.getoption("--C")
D = request.config.getoption("--D")
return C, D
Here, we use a fixture to define the non_parametrized_inputs
into our test function. However, it is not strictly necessary to load the options with a fixture. In the example below, I load the --ALL
option directly in my test function.
3. Parameterizing Tests and Filtering
Let’s define a test function that:
- Uses parameterized inputs (
A
andB
). - Filters test cases based on user-specified values of
--A
and--B
. - Dynamically uses
--C
and--D
for customization.
# non_parametrized_inputs() is defined above
# Parameterized test with A and B
@pytest.mark.parametrize("A, B", [
(1, 2),
(3, 4),
(5, 6),
])
def test_function(A, B, non_parametrized_inputs, request):
# Get custom A and B filters from CLI
A_filter = request.config.getoption("--A")
B_filter = request.config.getoption("--B")
run_all = request.config.getoption("--ALL")
if not run_all:
# Skip test cases that don't match the specified A and B
if A_filter is not None and str(A) != A_filter:
pytest.skip(f"Skipping {A=}, {B=} because it doesn't match --A={A_filter}")
if B_filter is not None and str(B) != str(B_filter):
pytest.skip(f"Skipping {A=}, {B=} because it doesn't match --B={B_filter}")
# Get C and D values from the fixture
C, D = non_parametrized_inputs
print(f"A={A}, B={B}, C={C}, D={D}")
# Add your test logic here
assert isinstance(C, bool)
assert isinstance(D, bool)
Again, requests
is used to access the CLI inputs.
Key Features of this function:
- Parameterized Inputs:
@pytest.mark.parametrize
generates multiple test cases forA
andB
.
- If the
--ALL
flag is not given, all tests will be skipped unless the filter conditions are satisfied. - Dynamic Filtering:
- Users can run specific tests using
--A
and--B
options. This is equivalent to calling the tests withpytest mytest.py -k
- Users can run specific tests using
- Default and Custom Values:
--C
and--D
allow customization while falling back to defaults when not specified.
4. Running Tests: Command-Line Examples
Run All Tests with Defaults:
# All tests are skipped due to the test filters
pytest -s mytest.py
# Turn off the filters and run all tests
pytest -s mytest.py --ALL
Override C to be True:
pytest -s mytest.py --C
Override C and D:
pytest -s mytest.py --C --D
Run a specific test (e.g., A=3 and B=4):
pytest -s mytest.py --A=3 --B=4
Run a specific parametrized test
pytest -s mytest.py::test_function[1-2]
Run a specific parametrized test with the k
flag
# run only the test that matches the call "mytest.py::test_function[3-4]"
pytest -s mytest.py -k 3
Bonus: Running Tests in the VS Code UI
To execute tests with custom arguments in the VS Code UI:
- Set Up Pytest in VS Code:
- Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P on Mac).
- Search for and select Python: Configure Tests.
- Choose pytest and point it to your test directory.
- Run Tests:
- Use the Testing sidebar (Ctrl+Shift+T or Cmd+Shift+T).
- Click on the test(s) you want to run.
- Passing Arguments:
Edit the .vscode/settings.json file in your workspace to include Pytest arguments:
{
"python.testing.pytestArgs": [
"--A=3",
"--B=4",
"--C=my_C_value",
"--D=my_D_value"
]
}
Putting it all together
In conclusion, I’ve demonstrated a few ways to specifify test parameters.
- We use
parser.addoption(--myOption)
to pass inmyOption
from the command line, and retrieve it withrequest.config.getoption(--myOption)
. - Using custom defined filters in combination with
pytest.mark.parametrize
allow us to run multiple or specific tests. - The built-in pytest filter is the
-k
flag (docs). This is the preferred way to filter specific tests to run. It’s usually better to use the-k
flag rather than using the custom filters if you want to run specific tests.