DevOps

Enhancing unit tests with pytest: tips, tools, and techniques

Jacob Sevlie
November 11, 2024

Unit testing is a critical part of ensuring that your code is reliable, maintainable, and bug-free. Pytest, one of the most popular testing frameworks in Python, offers a wide range of features and plugins that make writing and maintaining tests both efficient and straightforward. In this blog post, we'll explore some key tips and tools for writing effective unit tests using Pytest. We'll cover useful packages like Faker, Factory Boy, and Freezegun, and delve into general approaches that can help you create robust and reliable tests. Whether you're a seasoned developer or just getting started with testing, these insights will help you elevate your Pytest skills to the next level.

Useful packages

Here’s a set of packages that can enhance your testing process: 

  • faker 
  • factory-boy 
  • freezegun 
  • pytest-mock 
  • pytest-randomly 
  • pytest-repeat 
  • ipython 

General approaches

Writing quick and simple unit tests for each method might seem like overkill, but it provides a tight feedback loop for verifying how a single method works. . If someone makes a change, or a method is removed, the associated tests should go with it. Since the maintenance of code is a collective responsibility, having thorough coverage can give you more confidence to safely make changes and better understand how everything functions.

  • Use pytest-repeat (https://github.com/pytest-dev/pytest-repeat) to prove you can run a test 500 times in a row without failure. Sometimes failures happen every so often, and you might think"I wish I could run this a hundred times to be sure". This tool allows you to do just that:
  • Use pytest-mock  (https://github.com/pytest-dev/pytest-mock) for all of your patching and mocking needs, preferring mocker over mock . Avoid using decorators, but instead put the mocks directly in your methods; this helps minimize the signature and allows you greater control. Use mock contexts for additional explicitness and scoping within the method itself. Try not to mock too much -- or too far up the chain -- unless not doing so will require you to have much more data instantiation.
  • Use pytest-randomly (https://github.com/pytest-dev/pytest-randomly) to randomize the order of test executions each time you run your tests. . Individual unit tests should be self-contained, able to run by itself, and not have any bleeding from other tests. If you take a big set of tests that have been running successfully for a long time and then randomize them, you might see one of them fail. An example of this is an environment variable being incorrectly assigned (read: not mocked) and when the tests run in a specific order, it will work; then you randomly mix them up, and they fail.
  • Use freezegun (https://github.com/spulec/freezegun) when writing time-specific tests. It's not just about time zones (although frequently can be), but about proving a piece of code runs in a known deterministic way at a specific date/time. Think about scripts needing special treatment around holidays, or weekends, or leap years. Being able to set up your unit test to make it appear it's running with the actual datetime is immensely useful. Remember, be aware of the datetimes being set in the database, too -- because this can sometimes cause issues. 

Additional guidance

Avoid directly overwriting environment variables in your test:

This will cause the variable to persist beyond the test. Bad practice.

Instead, use mocker (via the pytest-mock package) as a standalone line in your unit test:

"Factories as fixtures”. This can be useful in specific circumstances, but the more complex they become, the more it makes sense to use FactoryBoy traits instead. Parametrize the same behavior, and have different tests for different behaviors. Once you start doing if/else blocks in your tests against different parameterized values, it's probably time for a new test. 

Use identifiers on your parameterized tests to make them easier to describe. This is especially true if you have a complicated parameterization:

When you run the tests, the results will be more readable:

Without the ids parameter, the results are less readable:

Do not modify fixture values in other fixtures.
Modify and add to fixture values in a test itself, but never modify a fixture value in another fixture. Instead, aim to copy the fixture with deepcopy. This will help avoid unexpected behaviors.

Know how to use side_effects 

This can be extremely useful when needing to return different values in a mock based on the inputs given.

Use pass-throughs on mocks to prove a method was run while still allowing it to operate normally. You're basically "wrapping" a method with your mock, without blocking the method.

Conclusion

Leveraging Pytest and integrating the right tools and practices can lead to significant improvement to the quality and reliability of unit tests. By integrating packages such as pytest-mock and pytest-randomly, and applying advanced techniques like parametrization and mocking, a testing environment can be established that provides confidence in code performance and stability. This approach ensures that the testing process remains comprehensive and manageable, facilitating more reliable development and deployment.

Related Insights