Five reasons to use Py.test

Pytest library provides a better way to write tests, run the tests, and report the test results. This post is a comparison between the Python unit test standard library and pytest features and leaves out other libraries like nose2.

TL;DR

  • Single assert statement(assert a == b) over 40 different assert methods(self.assertEqual, self.assertIsInstance, self.assertDictEqual)
  • Better and detailed error messages on failure.
  • Useful command line options for reporting, discovering, and reporting tests like --last-failed, --collect-only.
  • Pytest plugins for extending the pytest functionalities and modifying default behavior. pytest-mon, pytest-clarity, pytest-cov
  • Pytest fixtures for seed data and implementation custom test behaviors

1. Single assert statement over 40 different assert methods

Here is a sample unittest code

import unittest


class TestUnitTestShowCase(unittest.TestCase):
		def test_equal(self):
				v1 = "start"
				v2 = "start+"

				self.assertEqual(v1, v2)

    def test_dictionary(self):
        rust = {'name': 'Rust', 'released': 2010}
        python = {'name': 'Python', 'released': 1989}

        self.assertDictEqual(rust, python)

    def test_list(self):
        expected_genres = ['Novel', 'Literary Fiction']
        returned_genres = ['Novel', 'Popular Fiction']

        self.assertListEqual(expected_genres, returned_genres)

TestCase class supports 40 different assert methods(https://docs.python.org/3/library/unittest.html). assertDictEqual method for comparing the equality of two dictionaries, the assertListEqual method for comparing the equality of two lists, and the assertEqual method is a superset for all comparisons. It can act on two variables that implement an equality check. So it’s technically possible to use assertEqual over assertDictEqual and assertListEqual. It becomes quite daunting to remember which assertMethod to use. The one advantage of using special assert methods is they check the arguments' type before comparing the value. For example, self.assertDictEqual("", "") will fail because the first argument is not dictionary.

Pytest recommends using assert statement over any specialized function or method. Here is an example of pytest testcases.

def test_dictionary():
    rust = {'name': 'Rust', 'released': 2010}
    python = {'name': 'Python', 'released': 1989}

    assert rust == python

def test_list():
    expected_genres = ['Novel', 'Literary Fiction']
    returned_genres = ['Novel', 'Popular Fiction']

    assert expected_genres == returned_genres

One assert statement can check all types of equality. It’s simple and easy to use and remember.

Pytest also supports executing the Tests inheriting unittest.TestCase with unit test assert methods or assert statements. You can write Pytest tests as a function.

2. Better Failure messages in Pytest

Consider the unittest two tests. One which checks two dictionaries are equal and two data class instances are equal.

import unittest

from dataclasses import dataclass

@dataclass
class Book:
    name: str
    year: int
    author: str


class TestUnitTestShowCase(unittest.TestCase):
    def test_assert_equal(self):
        expected_json = {'name': 'Haruki Murakami',
                         'language': 'Japanese',
                         'title': 'Windup Bird Chronicle',
                         'year_of_release': 1994,
                         'page_count': 607,
                         'genres': ['Novel', 'Science Fiction',
                                    'Pyschological Fiction']}
        return_json = {'name': 'Haruki Murakami',
                       'language': 'Japanese',
                       'title': 'Kafka on the shore',
                       'year_of_release': 2002,
                       'page_count': 505,
                       'genres': ['Novel', 'Magical Realism',
                                  'Fantasy Fiction']}

        self.assertDictEqual(return_json, expected_json)

    def test_dataclass(self):
        windup = Book(name='Windup Bird Chronicle', year=1994,
                      author='Haruki Murakami')
        kafka = Book(name='Kafka on the shore', year=2002,
                     author='Haruki Murakami')

        self.assertEqual(windup, kafka)


if __name__ == "__main__":
    unittest.main()

Output

$python test_unittest.py
FF
======================================================================
FAIL: test_assert_equal (__main__.TestUnitTestShowCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_unittest.py", line 41, in test_assert_equal
    self.assertDictEqual(return_json, expected_json)
AssertionError: {'nam[52 chars]e': 'Kafka on the shore', 'year_of_release': 2[77 chars]on']} != {'nam[52 chars]e': 'Windup Bird Chronicle', 'year_of_release'[86 chars]on']}
- {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction'],
+ {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction'],
   'language': 'Japanese',
   'name': 'Haruki Murakami',
-  'page_count': 505,
?                ^ ^

+  'page_count': 607,
?                ^ ^

-  'title': 'Kafka on the shore',
+  'title': 'Windup Bird Chronicle',
-  'year_of_release': 2002}
?                     ^^^^

+  'year_of_release': 1994}
?                     ^^^^


======================================================================
FAIL: test_dataclass (__main__.TestUnitTestShowCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_unittest.py", line 49, in test_dataclass
    self.assertEqual(windup, kafka)
AssertionError: Book(name='Windup Bird Chronicle', year=1994, author='Haruki Murakami') != Book(name='Kafka on the shore', year=2002, author='Haruki Murakami')

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=2)

The differing values on the first argument start with - minus sign and the second argument start with + sign.

When two dictionary values are different, the error message prints each key and value. Even though the deviating values are printed, it’s not straightforward to grasp the unit test runner output errors.

When the diff gets longer, the unit test redacts the error message. The behavior is painful when the test fails in the CI which takes a long time to run and rerun after changing the configuration.

Here is the modified test_assert_equal.

    def test_assert_equal(self):
        expected_json = {'name': 'Haruki Murakami',
                         'language': 'Japanese',
                         'title': 'Windup Bird Chronicle',
                         'year_of_release': 1994,
                         'page_count': 607,
                         'genres': ['Novel', 'Science Fiction',
                                    'Pyschological Fiction'],
                         'translations': {'en': ['Jay Rubin'],
                                          'translators': {'name': 'Jay Rubin',
                                                          'location': 'tokyo'}}}
        return_json = {'name': 'Haruki Murakami',
                       'language': 'Japanese',
                       'title': 'Kafka on the shore',
                       'year_of_release': 2002,
                       'page_count': 505,
                       'genres': ['Novel', 'Magical Realism',
                                  'Fantasy Fiction'],
                       'translations': {'ta': ['Nilavan']}}

        self.assertDictEqual(return_json, expected_json)

Output

$python test_unittest.py
FF
======================================================================
FAIL: test_assert_equal (__main__.TestUnitTestShowCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_unittest.py", line 45, in test_assert_equal
    self.assertDictEqual(return_json, expected_json)
AssertionError: {'nam[52 chars]e': 'Kafka on the shore', 'year_of_release': 2[114 chars]n']}} != {'nam[52 chars]e': 'Windup Bird Chronicle', 'year_of_release'[184 chars]o'}}}
Diff is 696 characters long. Set self.maxDiff to None to see it.
...

Output after setting maxDiff=None

...
AssertionError: {'nam[52 chars]e': 'Kafka on the shore', 'year_of_release': 2[114 chars]n']}} != {'nam[52 chars]e': 'Windup Bird Chronicle', 'year_of_release'[184 chars]o'}}}
- {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction'],
+ {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction'],
   'language': 'Japanese',
   'name': 'Haruki Murakami',
-  'page_count': 505,
?                ^ ^

+  'page_count': 607,
?                ^ ^

-  'title': 'Kafka on the shore',
+  'title': 'Windup Bird Chronicle',
-  'translations': {'ta': ['Nilavan']},
?                    ^^     ^ ----   -

+  'translations': {'en': ['Jay Rubin'],
?                    ^^     ^^^^^^^

+                   'translators': {'location': 'tokyo', 'name': 'Jay Rubin'}},
-  'year_of_release': 2002}
?                     ^^^^

+  'year_of_release': 1994}
?                     ^^^^

Now there is a new element translations inside the dictionary. Hence the error message is longer. The translators details are missing in one of the dictionary and it’s hard to find out from the error message.

The second test test_dataclass failure message doesn’t say which of the attributes are different.

AssertionError: Book(name='Windup Bird Chronicle', year=1994, author='Haruki Murakami')
!= Book(name='Kafka on the shore', year=2002, author='Haruki Murakami')

It’s hard to figure out what attributes values are different when the data class contains 15 attributes.

On the other hand, pytest error messages are clear and explicitly states difference with differing verbose level.

Here is the same test case for running with Pytest.

from dataclasses import dataclass

@dataclass
class Book:
    name: str
    year: int
    author: str


def test_assert_equal():
    expected_json = {'name': 'Haruki Murakami',
                     'language': 'Japanese',
                     'title': 'Windup Bird Chronicle',
                     'year_of_release': 1994,
                     'page_count': 607,
                     'genres': ['Novel', 'Science Fiction',
                                'Pyschological Fiction']}
    return_json = {'name': 'Haruki Murakami',
                   'language': 'Japanese',
                   'title': 'Kafka on the shore',
                   'year_of_release': 2002,
                   'page_count': 505,
                   'genres': ['Novel', 'Magical Realism',
                              'Fantasy Fiction']}
    assert  return_json == expected_json

def test_dataclass():
    windup = Book(name='Windup Bird Chronicle', year=1994,
                  author='Haruki Murakami')
    kafka = Book(name='Kafka on the shore', year=2002,
                 author='Haruki Murakami')

    assert windup == kafka

Output

$pytest test_pytest.py
======================================================================================================================================== test session starts =========================================================================================================================================
platform darwin -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/user/code/personal/why-pytest
collected 2 items

test_pytest.py FF                                                                                                                                                                                                                                                                              [100%]

============================================================================================================================================== FAILURES ==============================================================================================================================================
_________________________________________________________________________________________________________________________________________ test_assert_equal __________________________________________________________________________________________________________________________________________

    def test_assert_equal():
        expected_json = {'name': 'Haruki Murakami',
                         'language': 'Japanese',
                         'title': 'Windup Bird Chronicle',
                         'year_of_release': 1994,
                         'page_count': 607,
                         'genres': ['Novel', 'Science Fiction',
                                    'Pyschological Fiction']}
        return_json = {'name': 'Haruki Murakami',
                       'language': 'Japanese',
                       'title': 'Kafka on the shore',
                       'year_of_release': 2002,
                       'page_count': 505,
                       'genres': ['Novel', 'Magical Realism',
                                  'Fantasy Fiction']}
>       assert  return_json == expected_json
E       AssertionError: assert {'genres': ['...nt': 505, ...} == {'genres': ['...nt': 607, ...}
E         Omitting 2 identical items, use -vv to show
E         Differing items:
E         {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction']} != {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction']}
E         {'year_of_release': 2002} != {'year_of_release': 1994}
E         {'page_count': 505} != {'page_count': 607}
E         {'title': 'Kafka on the shore'} != {'title': 'Windup Bird Chronicle'}
E         Use -v to get the full diff

test_pytest.py:25: AssertionError
___________________________________________________________________________________________________________________________________________ test_dataclass ___________________________________________________________________________________________________________________________________________

    def test_dataclass():
        windup = Book(name='Windup Bird Chronicle', year=1994,
                      author='Haruki Murakami')
        kafka = Book(name='Kafka on the shore', year=2002,
                     author='Haruki Murakami')

>       assert windup == kafka
E       AssertionError: assert Book(name='Wi...uki Murakami') == Book(name='Ka...uki Murakami')
E
E         Omitting 1 identical items, use -vv to show
E         Differing attributes:
E         ['name', 'year']
E
E         Drill down into differing attribute name:
E           name: 'Windup Bird Chronicle' != 'Kafka on the shore'...
E
E         ...Full output truncated (6 lines hidden), use '-vv' to show

test_pytest.py:46: AssertionError
====================================================================================================================================== short test summary info =======================================================================================================================================
FAILED test_pytest.py::test_assert_equal - AssertionError: assert {'genres': ['...nt': 505, ...} == {'genres': ['...nt': 607, ...}
FAILED test_pytest.py::test_dataclass - AssertionError: assert Book(name='Wi...uki Murakami') == Book(name='Ka...uki Murakami')
========================================================================================================================================= 2 failed in 0.05s ==========================================================================================================================================

The failure message contains not only the error message but the entire function definition to understand the failure along with enough mismatch information.

Let’s see the test_dataclass error.

  • Omitting 1 identical items, use -vv to show - Says out of all the attributes one attribute contains the same value.

  • Differing attributes: ['name', 'year'] - Says the name of the differing attributes.

  • Drill down into differing attribute name: - Next the error message drills down the differing attribute values.

  • name: 'Windup Bird Chronicle' != 'Kafka on the shore'... - Says the attribute name, prints two comparing values.

  • ...Full output truncated (6 lines hidden), use '-vv' to show - Now Pytest redacts the remaining error message but says how many more lines are present which is quite useful and developer can decide rerun with -vv flag. After passing -vv flag, the output is richer.

def test_dataclass():
        windup = Book(name='Windup Bird Chronicle', year=1994,
                      author='Haruki Murakami')
        kafka = Book(name='Kafka on the shore', year=2002,
                     author='Haruki Murakami')

>       assert windup == kafka
E       AssertionError: assert Book(name='Windup Bird Chronicle', year=1994, author='Haruki Murakami') == Book(name='Kafka on the shore', year=2002, author='Haruki Murakami')
E
E         Matching attributes:
E         ['author']
E         Differing attributes:
E         ['name', 'year']
E
E         Drill down into differing attribute name:
E           name: 'Windup Bird Chronicle' != 'Kafka on the shore'
E           - Kafka on the shore
E           + Windup Bird Chronicle
E
E         Drill down into differing attribute year:
E           year: 1994 != 2002
E           +1994
E           -2002

For dictionary mismatch, Pytest prints each key and value in the same way. By having these varying levels, it quicker for developers to fix the errors.

# without verbose error

E       AssertionError: assert {'genres': ['...nt': 505, ...} == {'genres': ['...nt': 607, ...}
E         Omitting 2 identical items, use -vv to show
E         Differing items:
E         {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction']} != {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction']}
E         {'year_of_release': 2002} != {'year_of_release': 1994}
E         {'page_count': 505} != {'page_count': 607}
E         {'title': 'Kafka on the shore'} != {'title': 'Windup Bird Chronicle'}
E         Use -v to get the full diff

# with verbose error

E       AssertionError: assert {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction'],\n 'language': 'Japanese',\n 'name': 'Haruki Murakami',\n 'page_count': 505,\n 'title': 'Kafka on the shore',\n 'translations': {'ta': ['Nilavan']},\n 'year_of_release': 2002} == {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction'],\n 'language': 'Japanese',\n 'name': 'Haruki Murakami',\n 'page_count': 607,\n 'title': 'Windup Bird Chronicle',\n 'translations': {'en': ['Jay Rubin'],\n                  'translators': {'location': 'tokyo', 'name': 'Jay Rubin'}},\n 'year_of_release': 1994}
E         Common items:
E         {'language': 'Japanese', 'name': 'Haruki Murakami'}
E         Differing items:
E         {'year_of_release': 2002} != {'year_of_release': 1994}
E         {'genres': ['Novel', 'Magical Realism', 'Fantasy Fiction']} != {'genres': ['Novel', 'Science Fiction', 'Pyschological Fiction']}
E         {'title': 'Kafka on the shore'} != {'title': 'Windup Bird Chronicle'}
E         {'page_count': 505} != {'page_count': 607}
E         {'translations': {'ta': ['Nilavan']}} != {'translations': {'en': ['Jay Rubin'], 'translators': {'location': 'tokyo', 'name': 'Jay Rubin'}}}
E         Full diff:
E           {
E            'genres': ['Novel',
E         +             'Magical Realism',
E         -             'Science Fiction',
E         ?              ^^^^ ^^
E         +             'Fantasy Fiction'],
E         ?              ^^ ^^^^         +
E         -             'Pyschological Fiction'],
E            'language': 'Japanese',
E            'name': 'Haruki Murakami',
E         -  'page_count': 607,
E         ?                ^ ^
E         +  'page_count': 505,
E         ?                ^ ^
E         -  'title': 'Windup Bird Chronicle',
E         +  'title': 'Kafka on the shore',
E         -  'translations': {'en': ['Jay Rubin'],
E         ?                    ^^     ^ ^^^^^^
E         +  'translations': {'ta': ['Nilavan']},
E         ?                    ^^     ^^^ ^^   +
E         -                   'translators': {'location': 'tokyo',
E         -                                   'name': 'Jay Rubin'}},
E         -  'year_of_release': 1994,
E         ?                     ^^^^
E         +  'year_of_release': 2002,
E         ?                     ^^^^
E           }

3. Useful command line options

Pytest comes with a lot of useful command-line options for execution, discovering tests, reporting, debugging, and logging.

Pytest has an option --last-failed which only runs the test which failed during the last execution.

Here is an example where one test fails.

$ pytest test_pytest.py
======================================================================================================================================== test session starts =========================================================================================================================================
platform darwin -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/user/code/personal/why-pytest
collected 2 items

test_pytest.py F.                                                                                                                                                                                                                                                                              [100%]

============================================================================================================================================== FAILURES ==============================================================================================================================================
_________________________________________________________________________________________________________________________________________ test_assert_equal __________________________________________________________________________________________________________________________________________

    def test_assert_equal():
..
E         ...Full output truncated (2 lines hidden), use '-vv' to show

test_pytest.py:29: AssertionError
====================================================================================================================================== short test summary info =======================================================================================================================================
FAILED test_pytest.py::test_assert_equal - AssertionError: assert {'genres': ['...nt': 505, ...} == {'genres': ['...nt': 607, ...}
==================================================================================================================================== 1 failed, 1 passed in 0.05s =====================================================================================================================================

The short summary says, 1 failed, 1 passed in 0.05s. Next time, while running the test, pytest --last-failed test_pytest.py executes only failed test from previous run.

$pytest --last-failed test_pytest.py
======================================================================================================================================== test session starts =========================================================================================================================================
platform darwin -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/user/code/personal/why-pytest
collected 2 items / 1 deselected / 1 selected
run-last-failure: rerun previous 1 failure

test_pytest.py F                                                                                                                                                                                                                                                                               [100%]

============================================================================================================================================== FAILURES ==============================================================================================================================================
_________________________________________________________________________________________________________________________________________ test_assert_equal __________________________________________________________________________________________________________________________________________

    def test_assert_equal():
...
test_pytest.py:29: AssertionError
====================================================================================================================================== short test summary info =======================================================================================================================================
FAILED test_pytest.py::test_assert_equal - AssertionError: assert {'genres': ['...nt': 505, ...} == {'genres': ['...nt': 607, ...}
================================================================================================================================== 1 failed, 1 deselected in 0.04s ===================================================================================================================================

The short summary says, 1 failed, 1 deselected in 0.04s

pytest --collect-only collect all the test files and test function/classes in the path. While reporting, pytest notifies files with the same name across different directories. For example, if there is a file test_models in the unit test directory and in the integration directory, pytest refuses to run the tests and also complains during the collect phase. The test file name should be unique across the project.

$tree
.
├── test
   └── test_unittest.py
├── test_pytest.py
└── test_unittest.py

1 directory, 3 files

$pytest --collect-only
======================================================================================================================================== test session starts =========================================================================================================================================
platform darwin -- Python 3.8.5, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /Users/user/code/personal/why-pytest
collected 4 items / 1 error / 3 selected

<Module test_pytest.py>
  <Function test_assert_equal>
  <Function test_dataclass>
<Module test_unittest.py>
  <UnitTestCase TestUnitTestShowCase>
    <TestCaseFunction test_assert_equal>
    <TestCaseFunction test_dataclass>

=============================================================================================================================================== ERRORS ===============================================================================================================================================
_______________________________________________________________________________________________________________________________ ERROR collecting test/test_unittest.py _______________________________________________________________________________________________________________________________
import file mismatch:
imported module 'test_unittest' has this __file__ attribute:
  /Users/user/code/personal/why-pytest/test_unittest.py
which is not the same as the test file we want to collect:
  /Users/user/code/personal/why-pytest/test/test_unittest.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
====================================================================================================================================== short test summary info =======================================================================================================================================
ERROR test/test_unittest.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

4. Pytest plugins

Pytest is built using pluggy framework and it’s built as a set of composable and extensible plugins. As a result, any developer can extend the functionality of the pytest runner and pytest test functionality. There are more than 300 plus plugins available.

Earlier in the blog post, I said, how the pytest error message text is better compared to the unittest. The default error rendering doesn’t support colorized diff and side by side comparison.

The two plugins pytest-icdiff and pytest-clarity improves the error message rendering and message text for better and quick understanding.

A sample pytest-icdiff output

A sample pytest-clarity output with enhanced error messages

Reporting

  • pytest-cov, https://github.com/pytest-dev/pytest-cov is pytest integration for coverage plugin. It supports generating coverage as HTML reports.

  • pytest-html, https://github.com/pytest-dev/pytest-html/ is a plugin for generating HTML test results. On failure, the plugin takes screenshots of the selenium test browser.

  • pytest-xdist, https://pypi.org/project/pytest-xdist/ allows entire python tests to run in multiple CPUs rather than running on a single process. The fixtures may need a little change depending on the implementation.

  • The default test discovery process works in most of the case, but for larger projects which takes significant time to run, running the new tests first will save a lot of time. When a test fails, the entire test can be stopped and rerun after fixing the error. When running in CI server, this functionality comes in handy for three reasons - quicker feedback, save a few dollars, and reduce CI waiting for pending PRs. pytest-mon, https://testmon.org/determining-affected-tests.html uses coverage to find out the modified code and schedules the modified code to run first.

5. Fixtures

Fixtures, https://docs.pytest.org/en/latest/fixture.html#fixture, are seed data for the tests. For example, to test an API endpoint, the test may need user account associated company and billing details. With fixtures, setting, and deleting the data after the tests are easier. Pytest supports five different scopes for these fixtures - function, class, module, package, and session. Here is an example from the docs for a SMTP connection fixture with session scope.

#conftest.py
@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

# test_module.py

def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes

Apart from creating the seed data for the database, it’s possible to create a mock or factory for testing with help of fixtures.

pytest-django provides a set of Django specific helpers and fixtures for writing Django application test cases. The pytest.mark.django_db marker allows only test cases marked to access the database. This is a handy feature for separating database access. django_assert_max_num_queries helper allows only n times to access a database in a test function or method. There are quite a few handy helpers in the package, pytest-django.

Overall, Pytest is a powerful, feature-rich library to write better test cases. The library uses more functions and decorators to implement, extend the core features. Especially writing and understanding fixtures involves a bit of a learning curve, at the same time, pytest fixtures are scalable and robust. The other powerful feature is function parameterization which can save a lot of boiler code.

In general, pytest is a far more powerful, extensible, and configurable testing framework compared to the unittest framework in the standard library. You can still inherit the TestCase and use pytest as a test runner. It’s worth investing time to learn and use it.

Links from the post

See also

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Get new posts by email: