Python Unit Testing: Project Setup

Python Unit Testing: Project Setup

Unit testing is a critical component of modern software development, enabling developers to catch bugs and ensure that their code behaves as expected. If you're new to unit testing or looking to improve your existing workflow, this guide will help you get started with unit testing in Python. In this post, we'll walk through the steps required to configure a Python project for effective unit testing. We'll cover the tools and libraries needed, how to write and run tests. By the end of this post, you'll have the knowledge and skills you need to start writing and running unit tests in Python.

Table of contents

  • Modules Used for Testing
  • Tests Discovery & Tests Naming Conventions
  • Commands to Run the Tests
  • pytest Config File
  • Setup Example
  • Conclusion

Modules Used for Testing

There are several modules (packages/libraries) that can be used to write and run your tests with.

Test frameworks / Test runners

  • Python's built-in module unittest:
    • Has built-in assertion and mocking APIs,
    • Compared to pytest it has a verbose assertion APIs,
    • No auto-discovery, you need to mark a folder as a module in order to be discovered,
    • No plugins support.
  • Standalone module pytest (recommended):
    • Has built-in assertion and mocking APIs,
    • Simpler assertion APIs compared to unittest,
    • Auto-discovery of test modules,
    • Can run unittest tests (great to support legacy tests),
    • Supports plugins.

Both are good, but it depends on what you want to test, one module can be a better choice over the other depending on the what you’re testing.

For the sake of being future proof, I recommend going with pytest which is better than unittest when it comes to running tests whilst having a big list of plugins which makes it easier to mock third party modules (in unittest you need to mock a lot to make a third-party module fully mocked).

Tests Discovery & Tests Naming Conventions

Test files needs to be discovered in order to be tested (ran), and there is a difference between how unittest and pytest are discovering tests.

In unittest:

  • Files needs to be in the same directory,
  • If it’s in a nested folder, it needs to be a module (by adding a __init__.py file).
  • Files' names needs to start with test_*.py (default),
  • You can override this pattern using -p CLI flag.
  • Test methods needs to start with the same pattern: test_*(self):,
  • Inside each test file, you need to have a main method/function:
if __name__ == '__main__':
    unittest.main()
  • Tests cases (test grouping) can be done via a class:
class MyFunctionalityTests(unittest.TestCase):

In pytest (recommended):

  • Files doesn’t need to be in the same directory,
  • Files' names should be prefixed or suffixed by test_*.py or *_test.py,
  • Test methods needs to start with a prefix: test_*(): or test_*(self): (if inside a class test case),
  • No need for a main method/function,
  • Tests cases (test grouping) can be done via a class;
class TestMyFunctionality:

Commands to Run the Tests

In unittest:

python3 -m unittest file_name.py

In pytest:

pytest

More ways on how to invoke tests in pytest can be found here.

pytest Config File

To set some configurations for pytest runner, you can create a config file with different formats, but for the sake of simplicity use either .pytest.ini or pytest.ini.

A sample configuration file:

[pytest]
testpaths = src test

More on configuration options can be found here.

Setup Example

Finally to the setup example, we will start by creating the required files and content of each file:

Create a directory if you're setting a new project:

mkdir my-python-project

Create a main.py file:

touch main.py
def custom_sum(first_number, second_number):
    print(f'Summing {first_number} and {second_number}')
    return first_number + second_number

if __name__ == '__main__':
    print(custom_sum(1, 1))

Create a main_test.py file:

touch main_test.py
import builtins

from pytest_mock import MockerFixture
from main import custom_sum

class TestCustomSum:
    def test_should_sum_two_positive_numbers_correctly(self, mocker: MockerFixture):
	    # Arrange
        print_spy = mocker.spy(builtins, 'print')

		# Act
        result = custom_sum(1, 1)

		# Assert
        print_spy.assert_called_once_with('Summing 1 and 1')
        assert result == 2

Create a requirements.txt file (dependency file like package.json in JS/TS based projects):

touch requirements.txt
pytest==7.2.2
pytest-mock==3.10.0

Install dependencies:

python3 -m pip install -r requirements.txt

Run tests:

pytest

Conclusion

In this guide, we've covered the essential steps required to configure your Python project for effective unit testing. We started by introducing the modules commonly used for testing in Python and discussed the importance of adhering to naming conventions to make your tests easy to discover and maintain. We then covered the commands you'll need to run your tests and how to create a pytest configuration file to customize your testing environment. Finally, we provided an example of how to set up your project for testing using the popular pytest framework.

By following these guidelines, you can ensure that your Python code is thoroughly tested, helping to catch bugs early and ensure that your software behaves as expected. Remember, unit testing is a critical component of modern software development, and investing the time to learn how to do it effectively can pay dividends in the long run.

You can find the code of this post in this repository.

As always, I hope you learned something.

Found this useful? feel free to share it with your friends.

Join the newsletter from to notify you of new posts and updates.

Like the post? consider buying us a coffee ❤️.