Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions changelog/1367.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
**Support for subtests** has been added.

:ref:`subtests <subtests>` are an alternative to parametrization, useful in situations where test setup is expensive or the parametrization values are not all known at collection time.

**Example**

.. code-block:: python

def test(subtests):
for i in range(5):
with subtests.test(msg="custom message", i=i):
assert i % 2 == 0


Each assert failure or error is caught by the context manager and reported individually.

In addition, :meth:`unittest.TestCase.subTest` is now also supported.

.. note::

This feature is experimental and will likely evolve in future releases. By that we mean that we might change how subtests are reported on failure, but the functionality and how to use it are stable.
1 change: 1 addition & 0 deletions doc/en/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Core pytest functionality
fixtures
mark
parametrize
subtests
tmp_path
monkeypatch
doctest
Expand Down
6 changes: 6 additions & 0 deletions doc/en/how-to/parametrize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ pytest enables test parametrization at several levels:
* `pytest_generate_tests`_ allows one to define custom parametrization
schemes or extensions.


.. note::

See :ref:`subtests` for an alternative to parametrization.

.. _parametrizemark:
.. _`@pytest.mark.parametrize`:

Expand Down Expand Up @@ -194,6 +199,7 @@ To get all combinations of multiple parametrized arguments you can stack
This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``,
``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators.


.. _`pytest_generate_tests`:

Basic ``pytest_generate_tests`` example
Expand Down
88 changes: 88 additions & 0 deletions doc/en/how-to/subtests.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.. _subtests:

How to use subtests
===================

.. versionadded:: 9.0

.. note::

This feature is experimental. Its behavior, particularly how failures are reported, may evolve in future releases. However, the core functionality and usage are considered stable.

pytest allows for grouping assertions within a normal test, known as *subtests*.

Subtests are an alternative to parametrization, particularly useful when test setup is expensive or when the exact parametrization values are not known at collection time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For expensive test setup, pytest has a solution in the form of scoped fixtures. Since regular parametrization is advantageous, e.g. allowing each param value to be tested individually by nodeid, I think we should not encourage subtests when there's a decent parametrization alternative.

The "values not known at collection time" case is what I think we should emphasize. There is indeed inherently no way to do "dynamic parametrization" after collection.



.. code-block:: python

# content of test_subtest.py


def test(subtests):
for i in range(5):
with subtests.test(msg="custom message", i=i):
assert i % 2 == 0

Each assertion failure or error is caught by the context manager and reported individually:

.. code-block:: pytest

$ pytest -q test_subtest.py


Note that it is possible to use ``subtests`` multiple times in the same test, or even mix and match with normal assertions
outside the ``subtests.test`` block:

.. code-block:: python

def test(subtests):
for i in range(5):
with subtests.test(msg="stage 1", i=i):
assert i % 2 == 0

assert func() == 10

for i in range(10, 20):
with subtests.test(msg="stage 2", i=i):
assert i % 2 == 0

.. note::

See :ref:`parametrize` for an alternative to subtests.


Typing
------

:class:`pytest.SubTests` is exported so it can be used in type annotations:

.. code-block:: python

def test(subtests: pytest.SubTests) -> None: ...

.. _parametrize_vs_subtests:

Parametrization vs Subtests
---------------------------

While :ref:`traditional pytest parametrization <parametrize>` and ``subtests`` are similar, they have important differences and use cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing from the comment above, I can see two ways we can approach this:

1 - Subtests are for "runtime parametrization"

Per comment above, subtests are useful when you have some data dynamically fetched in the test and want individualized reporting for each data value.

The idea here is that parametrization should be the go-to tool, but we offer this subtest tool for this particular scenario.

2 - Subtests are for "sub-testing"

By which I mean, subtests are for when you have one conceptual test, i.e. consider it a complete whole, but just want to break down its reporting to parts.


How do you see it? The reason I'm asking is that it can affect how we document the feature, what we recommend, etc.


Parametrization
~~~~~~~~~~~~~~~

* Happens at collection time.
* Generates individual tests.
* Parametrized tests can be referenced from the command line.
* Plays well with plugins that handle test execution, such as ``--last-failed``.
* Ideal for decision table testing.

Subtests
~~~~~~~~

* Happen during test execution.
* Are not known at collection time.
* Can be generated dynamically.
* Cannot be referenced individually from the command line.
* Plugins that handle test execution cannot target individual subtests.
13 changes: 5 additions & 8 deletions doc/en/how-to/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,14 @@ their ``test`` methods in ``test_*.py`` or ``*_test.py`` files.

Almost all ``unittest`` features are supported:

* ``@unittest.skip`` style decorators;
* ``setUp/tearDown``;
* ``setUpClass/tearDownClass``;
* ``setUpModule/tearDownModule``;
* :func:`unittest.skip`/:func:`unittest.skipIf` style decorators
* :meth:`unittest.TestCase.setUp`/:meth:`unittest.TestCase.tearDown`
* :meth:`unittest.TestCase.setUpClass`/:meth:`unittest.TestCase.tearDownClass`
* :func:`unittest.setUpModule`/:func:`unittest.tearDownModule`
* :meth:`unittest.TestCase.subTest` (since version ``9.0``)

.. _`pytest-subtests`: https://github.com/pytest-dev/pytest-subtests
.. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol

Additionally, :ref:`subtests <python:subtests>` are supported by the
`pytest-subtests`_ plugin.

Up to this point pytest does not have support for the following features:

* `load_tests protocol`_;
Expand Down
3 changes: 3 additions & 0 deletions doc/en/reference/fixtures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Built-in fixtures
:fixture:`pytestconfig`
Access to configuration values, pluginmanager and plugin hooks.

:fixture:`subtests`
Enable declaring subtests inside test functions.

:fixture:`record_property`
Add extra properties to the test.

Expand Down
13 changes: 13 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,19 @@ The ``request`` fixture is a special fixture providing information of the reques
:members:


.. fixture:: subtests

subtests
~~~~~~~~

The ``subtests`` fixture enables declaring subtests inside test functions.

**Tutorial**: :ref:`subtests`

.. autoclass:: pytest.SubTests()
:members:


.. fixture:: testdir

testdir
Expand Down
1 change: 1 addition & 0 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def directory_arg(path: str, optname: str) -> str:
"logging",
"reports",
"faulthandler",
"subtests",
)

builtin_plugins = {
Expand Down
1 change: 1 addition & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"pytest_catchlog",
"pytest_capturelog",
"pytest_faulthandler",
"pytest_subtests",
}


Expand Down
1 change: 0 additions & 1 deletion src/_pytest/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@ def _report_unserialization_failure(
raise RuntimeError(stream.getvalue())


@final
class TestReport(BaseReport):
"""Basic test report object (also used for setup and teardown calls if
they fail).
Expand Down
Loading
Loading