User Guide¶
This guide will lead you to your first CSV-file parametrized pytest test. It starts with designing your test, preparing your data, writing the test method and finally execute your new test.
The Scenario¶
Let’s say, you have to test this method:
from functools import reduce
from typing import List, Tuple, Union
def get_smallest_possible_container(
number_of_items: int,
dimensions_of_item: Tuple[int, int, int],
available_container_sizes: Union[List[int], Tuple[int, ...]] = (1000, 2500, 7500),
) -> int:
volume = reduce(lambda x, y: x * y, [*dimensions_of_item, number_of_items])
possible_containers = list(filter(lambda s: s >= volume, available_container_sizes))
if len(possible_containers) == 0:
raise ValueError("No container available") from None
return min(possible_containers)
Parts of the code are from a more complex example written for a German blog post. The example code is part of the source code and can be found unter tests/test_blog_example.py
. It is documented as test_blog_example
.
Prepare your data¶
Your test data resides in an CSV file. CSV files can have different formats, when it comes to:
Field separators and delimiters
Quoting
Line Termination
The class pytest_csv_params.dialect.CsvParamsDefaultDialect
defines a default CSV format that should fit most requirements:
import csv
class CsvParamsDefaultDialect(csv.Dialect): # pylint: disable=too-few-public-methods
delimiter = ","
doublequote = True
lineterminator = "\r\n"
quotechar = '"'
quoting = csv.QUOTE_ALL
strict = True
skipinitialspace = True
You can derive your own CSV format class from there (or from csv.Dialect
), if your files look any other.
Your test data for the method above could look like this:
"Test-ID", "Number of items", "Dimensions of item", "Expected Container Size", "Expect Exception?", "Expected Message"
"Small Container 1", "15", "1 x 2 x 3", "1000", "N", ""
"Small Container 2", "125", "2 x 2 x 2", "1000", "N", ""
"Small Container 3", "16", "3 x 4 x 5", "1000", "N", ""
"Medium Container", "17", "3 x 4 x 5", "2500", "N", ""
"Large Container 1", "2", "15 x 12 x 10", "7500", "N", ""
"Large Container 2", "1", "16 x 20 x 20", "7500", "N", ""
"Not fitting 1", "2", "16 x 20 x 18", "0", "Y", "No container available"
"Not fitting 2", "7501", "1 x 1 x 1", "0", "Y", "No container available"
We have a header line in the first line, that names the single columns
The column names are not good for argument names
The value in the dimensions column needs to be transformed in order to get tested
There is a column that tells if an exception is to be expected, and the last two lines expect one
Design and write the test¶
The test must call the get_smallest_possible_container
method with the right parameters. The CSV file has all information, but maybe not in the right format. We take care of that in a second.
The test may expect an exception, that should also be considered.
The parameters of the test method should reflect the input parameters for the method under test, and the expectations.
So let’s build it:
import pytest
def test_get_smallest_possible_container(
number_of_items: int,
dimensions_of_item: Tuple[int, int, int],
expected_container_size: int,
expect_exception: bool,
expected_message: str,
) -> None:
if expect_exception:
with pytest.raises(ValueError) as expected_exception:
get_smallest_possible_container(number_of_items, dimensions_of_item)
assert expected_exception.value.args[0] == expected_message
else:
container_size = get_smallest_possible_container(number_of_items, dimensions_of_item)
assert container_size == expected_container_size
The test could now get all parameters needed to execute the
get_smallest_container_method
, as well as for the expectationsBased on the expectation for an exception, the test goes in two different directions
Now it’s time for getting stuff from the CSV file.
Add the parameters from the CSV file¶
Here comes the csv_params()
decorator. But one step after the other.
import pytest
from pytest_csv_params.decorator import csv_params
@csv_params(
data_file=join(dirname(__file__), "assets", "doc-example.csv"),
id_col="Test-ID",
header_renames={
"Number of items": "number_of_items",
"Dimensions of item": "dimensions_of_item",
"Expected Container Size": "expected_container_size",
"Expect Exception?": "expect_exception",
"Expected Message": "expected_message",
},
data_casts={
"number_of_items": int,
"dimensions_of_item": get_dimensions,
"expected_container_size": int,
"expect_exception": lambda x: x == "Y",
},
)
def test_get_smallest_possible_container(
number_of_items: int,
dimensions_of_item: Tuple[int, int, int],
expected_container_size: int,
expect_exception: bool,
expected_message: str,
) -> None:
With the parameter
data_file
you point to your CSV fileWith the parameter
id_col
you name the column of the CSV file that contains the test case ID; the test case ID is shown in the execution logsWith the
header_renames
dictionary you define how a column is represented as argument name for your test method; the highlighted example transforms “Number of items” tonumber_of_items
The
data_casts
dictionary you define how data needs to be transformed to be usable for the test; you can uselambda
s or method pointers; all values from the CSV arrive asstr
All possible parameters are explained under Configuration, or more technically, in the source documentation of pytest_csv_params.decorator.csv_params()
.
The data_casts
method get_dimensions
looks like the following:
def get_dimensions(dimensions_str: str) -> Tuple[int, int, int]:
dimensions_tuple = tuple(map(lambda x: int(x.strip()), dimensions_str.split("x")))
if len(dimensions_tuple) != 3:
raise ValueError("Dimensions invalid") from None
return dimensions_tuple # type: ignore
The method is called during the test collection phase. If the ValueError
raises, the run would end in an error.
Execute the test¶
There is nothing special to do now. Just run your tests as always. Your run should look like this:
tests/test.py::test_get_smallest_possible_container[Small Container 1] PASSED [ 12%]
tests/test.py::test_get_smallest_possible_container[Small Container 2] PASSED [ 25%]
tests/test.py::test_get_smallest_possible_container[Small Container 3] PASSED [ 37%]
tests/test.py::test_get_smallest_possible_container[Medium Container] PASSED [ 50%]
tests/test.py::test_get_smallest_possible_container[Large Container 1] PASSED [ 62%]
tests/test.py::test_get_smallest_possible_container[Large Container 2] PASSED [ 75%]
tests/test.py::test_get_smallest_possible_container[Not fitting 1] PASSED [ 87%]
tests/test.py::test_get_smallest_possible_container[Not fitting 2] PASSED [100%]
Analyse test failures¶
Is it a failure for all test data elements or just for a few?
When only some tests fail, the Test ID should tell you where to look at