Open In App

Pytest Tutorial | Unit Testing in Python using Pytest Framework

Last Updated : 18 Apr, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In the process of development of applications, testing plays a crucial role. It ensures the developers that the application is error-free and the application is running as per the expectation. It should be done to ensure a seamless experience for the user. In this article, we will learn about testing Python applications using Pytest.

Testing Python Application Using Pytest

Below, we will cover the Testing Python Application Using Pytest in the below points in Python.

What is Pytest?

Pytest is an open-source testing framework that has redefined simplicity and efficiency in Python testing. Its popularity hinges on its ability to support simple unit tests and complex functional testing for applications. What sets Pytest apart is its minimalistic syntax and the ability to write test codes using Python’s assert statement, making tests readable and maintainable.

To install Pytest execute the below command in the terminal:

pip install pytest

Why Choose Pytest?

Testing is like the safety net for our code. It catches the bugs before they cause trouble. In Python, Pytest is the best tool for this. It’s simple to use and powerful enough for most needs. In this guide, we’ll explore Pytest through practical examples, which makes it easy to understand.

Basic Pytest Test

Here we are going to write a basic test. Pytest looks for tests in files named starting with ‘test_’ or ending with ‘_test.py’. We create a function reverse_text() that takes string input and return its reverse. After that we create another function test_reverse_test() which tests the function reverse_text(). Inside test_reverse_test() we assert that output of reverse_text(‘python’) is equal to ‘nohtyp’. If this is true, the test passes. If not, Pytest flags this is a failed test.

Python3
# test_reversal.py

def reverse_text(text):
    return text[::-1]

def test_reverse_text():
    assert reverse_text('python') == 'nohtyp'

Output: To run the above test we have to execute “pytest” in the terminal and we will get the below output.

Screenshot-2023-12-07-110947

When to Create Fixtures?

Fixtures in pytest are used to provide a fixed baseline upon which tests can reliably and repeatedly execute. Fixtures are reusable components that are used to set up a base state for tests. They are particularly useful for setting up complex objects, connecting to databases, or doing any setup that might be needed before your tests can run. They are ideal for:

  • Setting up external resources: like databases or network connections.
  • Initializing data: that is required across multiple test functions.
  • Configuring environment: such as setting up paths or external variables.

Example: In below example, we create a fixture named “my_fixture” which is defined using the “@pytest.fixture” decorator. This fixture function returns a list [1, 2, 3].

Python3
import pytest
@pytest.fixture
def my_fixture():
    return [1, 2, 3]
def test_sum(sample_data):
    assert sum(sample_data) == 6

When to Avoid Fixtures?

In Pytest, while knowing about the importance of creating fixtures it also crucial to know that where to avoid fixtures for the smooth testing.

  1. For overly simple setups: where the setup is just one or two lines of code.
  2. When they reduce test readability: if the fixture is used only once or its purpose is not clear.
  3. If they introduce unnecessary dependencies between tests, reducing the ability to run tests in isolation.

How to use Fixtures at Large Scale?

When dealing with extensive test suites, it’s crucial to manage fixtures efficiently:

  • Use “conftest.py”: Place common fixtures in a “conftest.py” file at the root of your test directory, making them accessible across multiple test files.
  • Fixture Scoping: Apply proper scoping (‘function’, ‘class’, ‘module’, ‘session’) to optimize setup and teardown operations.
  • Fixture Parametrization: Reuse fixtures with different inputs using params in the fixture decorator.

Enhancing Functionality and Tests with Pytest

Consider a scenario where a user enter non-string input in ‘reverse_text()’ function. Now, we refine the above function to raise an exception in such cases and test this behaviour.

In the below code, Pytest runs the test function ‘test_reverse_text_not_string()’. This test function calls ‘reverse_text(1234)’. As ‘1234’ is a number not a string so, ‘reverse_text()’ raises a ‘ValueError’. The ‘with pytest.raises(ValueError)’ in the test function is expecting this ‘ValueError’, that why the test passes.

Python3
# test_reversal2.py

import pytest

def reverse_text(text):
    if not isinstance(text, str):
        raise ValueError('Expected a string')
    return text[::-1]

def test_reverse_text_non_string():
    with pytest.raises(ValueError):
        reverse_text(1234)

Output:

Screenshot-2023-12-07-120349

Deep Dive with Pytest Fixtures

For a more complex example, Let’s create a Calculator class with methods to add, subtract, multiplication and division. We’ll explore how fixtures can manage setup and teardown in testing such scenarios.

Python3
# calculator.py

class Calculator:
    def add(self, a, b):
        """Return the addition of two numbers."""
        return a + b

    def subtract(self, a, b):
        """Return the subtraction of two numbers."""
        return a - b

    def multiply(self, a, b):
        """Return the multiplication of two numbers."""
        return a * b

    def divide(self, a, b):
        """Return the division of two numbers."""
        if b == 0:
            raise ValueError("Cannot divide by zero.")
        return a / b

Next, we create the test script using Pytest. In this script we will include tests for each arithmetic operation as written in the below code.

In the below code, first we import the pytest module and Calculator class that includes four methods including “add()”, “subtract()”, “multiply()”, and “divide()” for basic arithmetic operations. The “divide()” method raises a ‘ValueError’ if the divisor is zero, to handle the division by zero case. Pytest Fixture “calc” in the above code create a new instance of the ‘Calculator’ class before each test. This ensures that each test is run with a fresh ‘Calculator’ instance, which is a good practice to avoid dependencies between tests. Each test function such as ‘test_addition()’, ‘test_subtraction’ and more receives the ‘calc’ fixture as an argument. The test then use this ‘Calculator’ instance to test various arithmetic operations. For example, ‘test_addition()’ method checks whether the ‘add()’ method correctly adds two numbers or not.

Python3
# test_calculator.py

import pytest
from calculator import Calculator

@pytest.fixture
def calc():
    """Provides a Calculator instance."""
    return Calculator()

def test_addition(calc):
    """Test addition method."""
    assert calc.add(2, 3) == 5

def test_subtraction(calc):
    """Test subtraction method."""
    assert calc.subtract(5, 3) == 2

def test_multiplication(calc):
    """Test multiplication method."""
    assert calc.multiply(3, 4) == 12

def test_division(calc):
    """Test division method."""
    assert calc.divide(8, 2) == 4

def test_division_by_zero(calc):
    """Test division by zero raises ValueError."""
    with pytest.raises(ValueError):
        calc.divide(10, 0)

Output:

Screenshot-2023-12-07-123221



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads