Pytest Tutorial: Mastering Unit Testing in Python

Welcome to a ALL-IN-ONE Tutorial designed to meet all your testing requirements. Whether you’re just starting with the fundamentals to build a solid conceptual foundation or aiming to craft professional-grade test cases for entire projects, this guide has got you covered. The focus of this tutorial will be around the popular “Pytest” library.



Understanding Unit Testing

In the world of programming, a unit is the smallest part of your code, like a single function or method. Unit testing involves examining these individual parts to ensure they’re doing what they’re supposed to. It’s like putting each piece of the puzzle under a microscope to make sure it fits perfectly and does its job without causing trouble for the whole picture.

Imagine you’re building a complex building. The traditional approach might be to wait for the entire building to be completed, then performing tests on it to test its integrity. Unit testing on the other hand, would have you test the integrity of each floor as you build it.

Here is another scenario:

You have a function that’s supposed to add two numbers together. Unit testing for this function would involve giving it different pairs of numbers and checking if it consistently produces the correct sum. It’s like asking, “Hey, can you add 2 and 3? What about 0 and 0? Or even -1 and 1?” Each time, the unit test checks if the function gives the right answer. These different pairs of numbers must be defined carefully to ensure that the function works under a variety of different circumstances. For example, the first pair might use negative numbers, second pair might use positive numbers, and third pair might target an “error” case where a string and a number are used as inputs (with the expectation of the test failing).

Why bother with this meticulous process? Because, it helps catch bugs early on, before they turn into big, tangled problems.

Unit testing ensures that each building block of your code functions as expected, creating a solid foundation for your software structure. It’s a practice that developers swear by because it not only saves time but also makes your code more reliable.


Why Pytest?

Let’s explore some of the key features that make pytest a popular choice for testing in Python.


1. Automatic Test Discovery

One of pytest‘s strengths is its ability to automatically discover and run tests in your project. By default, pytest identifies files with names starting with “test_” or ending with “_test.py” and considers them as test modules. It then discovers test functions or methods within these modules.


2. Concise Syntax

pytest uses a simplified and expressive syntax for writing tests. Test functions don’t need to be part of a class, and assertions can be made using the assert statement directly. This leads to more readable and concise test code.


3. Parameterized Testing

Pytest supports parameterized testing, enabling developers to run the same test with multiple sets of inputs. This feature is incredibly beneficial for testing a variety of scenarios without duplicating test code.


And many more such benefits (that we can’t explain without getting too technical).


Getting Started with pytest

To get started with pytest, you need to install it. You can do this using pip, the Python package installer, with the following command:

pip install pytest

Once installed, you can run tests using the pytest command.


Pytest Tutorial: Writing your First Test

Let’s start by creating a basic test using pytest. Create a file named test_example.py with the following content:

# test_example.py
def add(x, y):
    return x + y

def test_add():
    assert add(2, 3) == 5
    assert add(0, 0) == 0
    assert add(-1, 1) == 0

In this example, we define a simple add function and a corresponding test function using pytest‘s assert statement. The test checks whether the add function produces the expected results for different input values.

To execute the tests, run the following command in your terminal:

pytest test_example.py

pytest will discover and run all test functions in the specified file, by looking for functions with the word “test” in their names. If the tests pass, you’ll see an output indicating success. Otherwise, pytest will provide detailed information about the failures.


Pytest Tutorial: Parameterized Testing

pytest supports parameterized testing, enabling you to run the same test with multiple sets of inputs. This is achieved using the @pytest.mark.parametrize decorator.

import pytest

def add(x, y):
    return x + y

@pytest.mark.parametrize("input_a, input_b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_add(input_a, input_b, expected):
    result = add(input_a, input_b)
    assert result == expected

In this example, the test_add function is executed three times with different input values, reducing code duplication and making it easier to cover various scenarios.


Pytest Tutorial: Command-Line Options

pytest provides a plethora of command-line options to customize test runs. For example, you can specify the directory or files to test, run specific tests or test classes, and control the verbosity of the output.

Here are key command-line options:


Specifying Test Directories or Files:

Use the -k option to specify a substring match for test names. For instance:

pytest -k test_module

This command runs all tests containing “test_module” in their names.

Specify a specific directory or file to test:

pytest tests/test_module.py

Execute tests from a specific file or directory, allowing targeted testing.


Controlling Verbosity:

Adjust the verbosity level with the -v option to get more detailed output:

pytest -v

Display test names and results. Useful for understanding test execution flow.

Increase verbosity for even more detailed information:

pytest -vv

Provide additional information about skipped tests and setup/teardown stages.


Marking and Selecting Tests:

Utilize custom markers to categorize and selectively run tests. For example:

pytest -m slow

This command runs tests marked with @pytest.mark.slow, allowing you to separate and focus on tests specifically categorized as slow-running.

Select tests based on their outcome, such as only running failed tests:

pytest --lf

Run only the tests that failed in the last test run.


Parallel Test Execution:

Speed up test runs by leveraging parallel execution:

pytest -n auto

This command runs tests in parallel, utilizing all available CPU cores.


Generating Detailed Reports:

Generate detailed reports in various formats, such as HTML or XML:

pytest --html=report.html

This command produces an HTML report for a more visual representation of test results, aiding in result analysis and sharing with stakeholders.


These command-line options empower developers to fine-tune their testing processes, making Pytest a flexible and customizable tool for projects of any scale. Whether you need to run specific tests, control output verbosity, or generate comprehensive reports, Pytest’s command-line options provide the versatility needed for efficient and effective testing.


How to use Pytest effectively in Larger Projects

As the size of your project grows, with the number of files and lines of code increasing significantly, the need to organize your code becomes even more important. While it may seem tempting to write all your “test” functions in the same file as your regular code, this is not an ideal solution.

Instead, it is recommended to create a separate file where all of your tests are written. Depending on the size of the project, you can even have multiple test files (e.g. one test file for each class).

Opting for this approach introduces potential complications. When test cases are written in a separate file, a common concern arises: How do we invoke the functions intended for testing?

This requires careful structuring of your project and code to ensure that individual functions and classes of your project can be imported by the pytest files.

Here is a good project structure to follow, where each of the files in src folder represent an independent module (e.g. a single class), and each of the files in the tests folder corresponds to a file in the src folder.

project_root/
|-- src/
|   |-- __init__.py
|   |-- users.py
|   |-- services.py
|
|-- tests/
|   |-- __init__.py
|   |-- test_users.py
|   |-- test_services.py
|
| -- main.py

The __init__.py file is an important addition to the src folder, where all of our project files are stored (excluding the main driver code). When this file is created in a folder, that folder will be recognized by Python as a Python Package, and enables other files to import files from within this folder. You do not have to put anything in this file (leave it empty, though it can be customized with special statements).

It is also necessary to put the __init__.py file in the tests folder, in order for imports between it, and the src folder to succeed.

Example scenario: Importing the users.py file from test_users.py.

from src.users import *

Conclusion

This marks the end of the Pytest tutorial.

By incorporating unit testing into your development workflow, you can catch and fix bugs early, improve code maintainability, and ensure that your software functions as intended. With pytest, the journey of mastering unit testing in Python becomes not only effective but also enjoyable. So, go ahead, write those tests, and build robust, reliable Python applications with confidence!

Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments