Debugging and testing Python code with Pytest

Python is one of the most popular programming languages, thanks to its simplicity, versatility, and large community of developers. However, as the complexity of a project increases, it becomes more difficult to ensure that the code works as expected and that changes to the code don’t break existing functionality. That’s where debugging and testing come in. In this article, we’ll explore how to use Pytest, one of the most popular testing frameworks for Python, to test and debug your code.
Introduction to Pytest
Pytest is a testing framework for Python that makes it easy to write and run tests for your code. It provides a rich set of features for testing different types of code, from simple functions to complex applications. With Pytest, you can write tests using a simple and intuitive syntax, and run them with a single command. Additionally, Pytest has a large and active community of users, which means that you can find many resources and examples to help you get started.
Setting up Pytest
Before you can start using Pytest, you need to install it. You can do this using the pip package manager by running the following command in your terminal or command prompt:
pip install pytest
Once you’ve installed Pytest, you’re ready to start writing and running tests. To do this, you need to create a Python file with your tests and run the Pytest command with the name of the file as an argument. For example, if you have a file named test_example.py
, you can run the tests in that file by running the following command:
pytest test_example.py
Writing tests with Pytest
Pytest tests are simply Python functions that start with the test_
prefix. When you run the Pytest command, it will search for these functions in your code and run them automatically. To write a test, you simply write a function that asserts that a particular condition is true. For example, the following code defines a test that checks whether the add
function correctly adds two numbers:
def add(a, b):
return a + b
def test_addition():
assert add(1, 2) == 3
In this example, the test_addition
function uses the assert
statement to check that the result of calling the add
function with the arguments 1
and 2
is equal to 3
. If the result of the add
function is not equal to 3
, the assert
statement will raise an exception, and Pytest will report that the test has failed.
Running tests with Pytest
Once you’ve written your tests, you can run them by using the Pytest command. For example, if you have a file named test_example.py
with the test code shown above, you can run the tests by running the following command:
pytest test_example.py
When you run this command, Pytest will search for all the functions in the file that start with the test_
prefix, and run each one. After each test is run, Pytest will display a message indicating whether the test passed or failed. For example, you might see the following output after running the tests in the previous example:
============================= test session starts ==============================
platform darwin -- Python 3.8.6, pytest-6.2.2, py-1.10
collected 1 item
test_example.py . [100%]
============================== 1 passed in 0.01s ===============================
In this output, the .
after the file name test_example.py
indicates that the test passed. If a test had failed, the .
would be replaced with an F
. Additionally, Pytest will display a detailed message indicating what went wrong and where the failure occurred.
Fixtures in Pytest
Pytest fixtures are a powerful feature that allow you to share resources between tests. For example, you might want to create a fixture that sets up a database connection, so that all of your tests can use that connection without having to repeat the setup code for each test. To create a fixture, you define a function that returns the resource you want to share, and decorate the function with the @pytest.fixture
decorator.
For example, the following code defines a fixture that creates a database connection:
import psycopg2
@pytest.fixture
def db_conn():
conn = psycopg2.connect(
host="localhost",
database="mydatabase",
user="mydatabaseuser",
password="mypassword"
)
yield conn
conn.close()
In this example, the fixture db_conn
uses the psycopg2
library to create a database connection to a PostgreSQL database. The yield
statement allows the fixture to be used as a context manager, so that the connection is automatically closed when the tests are done using it. To use this fixture in a test, you simply include it as an argument to the test function:
def test_insert_data(db_conn):
cursor = db_conn.cursor()
cursor.execute("INSERT INTO mytable (col1, col2) VALUES (1, 2)")
cursor.execute("SELECT * FROM mytable")
result = cursor.fetchall()
assert result == [(1, 2)]
In this example, the test_insert_data
function uses the db_conn
fixture to create a database cursor and insert some data into a table. It then queries the table to check that the data was correctly inserted, and asserts that the result is as expected.
Parametrized tests in Pytest
Another useful feature of Pytest is the ability to run a single test with multiple sets of inputs. This is called “parametrized testing”. You can use parametrized tests to test a single function or method with multiple combinations of inputs and expected results. To do this, you use the @pytest.mark.parametrize
decorator to specify the inputs and expected results for each test case.
For example, the following code defines a parametrized test that checks whether the add
function correctly adds two numbers:
def add(a, b):
return a + b
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(2, 3, 5),
(3, 4, 7),
(4, 5, 9),
])
def test_addition(a, b, expected):
assert add(a, b) == expected
In this example, the test_addition
function is decorated with @pytest.mark.parametrize
, which specifies four test cases, each with a different set of inputs and an expected result. When this test is run, Pytest will run the test_addition
function four times, once for each test case.
If any of the tests fail, Pytest will display a detailed error message indicating which test case failed and what the expected and actual results were.
Testing Exceptions in Pytest
In Python, exceptions are used to indicate errors or exceptional conditions that occur during the execution of a program. When testing code that raises exceptions, it’s important to ensure that the correct exceptions are being raised and that the error messages are correct.
To test exceptions in Pytest, you can use the pytest.raises
context manager. The pytest.raises
context manager allows you to test that a specific exception is raised by the code you’re testing. For example, the following code tests that the divide
function raises a ZeroDivisionError
when trying to divide by zero:
def divide(a, b):
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError, match="division by zero"):
divide(1, 0)
In this example, the test_divide_by_zero
function uses the pytest.raises
context manager to test that the divide
function raises a ZeroDivisionError
when trying to divide by zero. The match
argument to pytest.raises
allows you to specify a string that the error message should match. If the error message doesn’t match the specified string, the test will fail.
Testing output with Pytest
Sometimes you want to test the output of a function, rather than its return value. For example, you might want to test that a function is printing the correct output to the console. To test the output of a function, you can use the capsys
fixture, which captures the output of the function and makes it available for testing.
For example, the following code tests that the print_hello
function correctly prints the message “Hello, world!”:
def print_hello():
print("Hello, world!")
def test_print_hello(capsys):
print_hello()
captured = capsys.readouterr()
assert captured.out == "Hello, world!\n"
In this example, the test_print_hello
function uses the capsys
fixture to capture the output of the print_hello
function. The readouterr
method of the capsys
fixture returns a CapturedIO
object, which contains the output of the function as a string. The test asserts that the captured output is equal to the expected output.
Conclusion
Pytest is a powerful and flexible testing framework for Python. With its easy-to-use syntax, rich set of features, and large community of users and contributors, Pytest is an excellent choice for testing your Python code. Whether you’re testing simple functions or complex systems, Pytest makes it easy to write and run effective tests, so you can be confident that your code is working correctly.