DEV Community

Claret Ibeawuchi
Claret Ibeawuchi

Posted on

Understanding Tests in Python: A Comprehensive Guide Using Pytest

meme on test

Table of Contents

  1. What is a Test?
  2. Application Testing in Python with Pytest
  3. Conventions for Python Test Discovery
  4. Getting Started with Pytest
  5. Writing Tests with Pytest
    • 5.1 math_operations/math_operations.py
    • 5.2 tests/conftest.py
    • 5.3 tests/test_math_operations.py
    • 5.4 pytest.ini
  6. Understanding the Scripts
    • 6.1 math_operations/math_operations.py
    • 6.2 tests/conftest.py
    • 6.3 tests/test_math_operations.py
    • 6.4 pytest.ini
  7. Important Concepts
  8. Deployment Best Practice: Using Pytest with Tox
  9. Conclusion

What is a Test?

picture showing test completed

Pause for a moment and ponder: What is a test? According to Google, a test is "a procedure intended to establish the quality, performance, or reliability of something, especially before it is taken into widespread use." In simpler terms, a test evaluates the outcome of a specific behavior to ensure it aligns with expectations.

meme on test

Tests should be carried out before deployment to production.

The Four Steps of Testing

  1. Arrange: Prepare the test environment, set the stage for the desired behavior. This involves tasks such as object preparation, service initialization, or data entry into a database.

  2. Act: Trigger the singular, state-changing action that initiates the behavior to be tested. Typically, this involves calling a function or method.

  3. Assert: Examine the resulting state to confirm it aligns with expectations. Assertions validate if the behavior meets the anticipated outcome. For example, assert result == "5".

  4. Cleanup: Ensure the test environment returns to its initial state to prevent unintended influence on subsequent tests.

At its core, a test revolves around the Act and Assert steps, with Arrange providing the necessary context. The behavior under scrutiny unfolds between the Act and Assert phases.

Application Testing in Python with Pytest

Now, let's delve into application testing in Python using Pytest, a powerful, flexible, and extensible testing framework.

Introduction to Pytest

Pytest stands out as a preferred testing framework due to its simplicity and versatility. Its features include:

  • Easy to Use: Pytest boasts a straightforward and intuitive syntax, making test creation and execution enjoyable.

  • Flexible: Suited for testing a wide array of applications, Pytest supports various fixtures, assertions, and markers.

  • Extensible: Highly customizable with numerous third-party plugins, extending its functionality.

Key Features of Pytest

  1. Improved Test Coverage: Easily write tests covering all facets of your application, enhancing overall code quality.

  2. Reduced Regression Risk: Identify and rectify regressions early in development, saving time and effort.

  3. Increased Developer Productivity: Streamlined test creation and execution free up developers to focus on other tasks.

Alternatives to Pytest

  1. Unittest:

    • Pros: Comes with the standard library, well-integrated with the Python ecosystem.
    • Cons: Limited built-in fixtures, more boilerplate code compared to Pytest.
  2. Nose:

    • Pros: Extends unittest for easier testing, features a plugin architecture for extensibility.
    • Cons: Less actively developed than Pytest, some feature overlap with unittest.

Conventions for Python Test Discovery

Before diving into Pytest, understanding its test discovery conventions is essential. Pytest follows a standard discovery process:

  • Collection starts from testpaths or the current directory.
  • Recursion into directories, excluding those listed in norecursedirs.
  • Searching for test_*.py or *_test.py files, imported by their test package name.
  • Collecting test items, including test functions and methods.

For optimal test discovery, name your pytest scripts as test_*.py or *_test.py for readability and consistency.

Getting Started with Pytest

Prerequisites

Ensure you have:

  • Basic Python knowledge
  • Understanding of Pip
  • Text Editor or IDE

Installation

Install Pytest using pip:

pip install pytest
Enter fullscreen mode Exit fullscreen mode

Writing Tests with Pytest

Consider the following directory structure:

pytest-article/
│
├── math_operations/
│   ├── __init__.py
│   ├── math_operations.py
│
├── tests/
│   ├── conftest.py
│   ├── __init__.py
│   ├── test_math_operations.py
│
└── pytest.ini
Enter fullscreen mode Exit fullscreen mode

math_operations/math_operations.py

# Code defining basic mathematical operations and a Calculator class
def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

class Calculator:
    def multiply(self, x, y):
        return x * y

    def divide(self, x, y):
        if y == 0:
            raise ValueError("Cannot divide by zero")
        return x / y
Enter fullscreen mode Exit fullscreen mode

tests/conftest.py

# Fixture to create an instance of the Calculator class for testing
import pytest
from math_operations.math_operations import Calculator

@pytest.fixture
def calculator_instance():
    return Calculator()
Enter fullscreen mode Exit fullscreen mode

tests/test_math_operations.py


import pytest
from math_operations.math_operations import add, subtract

# Fixture to set up some common data for the tests
@pytest.fixture
def common_data():
    return {'x': 10, 'y': 5}

# Test the add function
 add function
def test_add(common_data):
    result = add(common_data['x'], common_data['y'])
    assert result == 15

# Test the subtract function
def test_subtract(common_data):
    result = subtract(common_data['x'], common_data['y'])
    assert result == 5

# Test the multiply method of the Calculator class
def test_calculator_multiply(calculator_instance, common_data):
    result = calculator_instance.multiply(common_data['x'], common_data['y'])
    assert result == 50

# Test the divide method of the Calculator class
def test_calculator_divide(calculator_instance, common_data):
    result = calculator_instance.divide(common_data['x'], common_data['y'])
    assert result == 2.0

# Test division by zero using a marker
@pytest.mark.divide_by_zero
def test_calculator_divide_by_zero(calculator_instance, common_data):
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        calculator_instance.divide(common_data['x'], 0)

# Use marks to categorize tests
@pytest.mark.parametrize("input_x, input_y, expected_result", [(2, 3, 5), (0, 0, 0), (-2, -3, -5)])
def test_add_parametrized(input_x, input_y, expected_result):
    result = add(input_x, input_y)
    assert result == expected_result
Enter fullscreen mode Exit fullscreen mode

pytest.ini

[pytest]
testpaths = tests # define test paths
markers =
    divide_by_zero: custom marker for tests that involve division by zero
addopts = -v # make output verbose
Enter fullscreen mode Exit fullscreen mode

Run tests using:

pytest
Enter fullscreen mode Exit fullscreen mode

Result of test output

Understanding the Scripts

  • math_operations/math_operations.py: Defines basic mathematical operations and a Calculator class.

  • tests/conftest.py: Provides a fixture, calculator_instance, creating a Calculator instance for testing.

  • tests/test_math_operations.py: Tests functions and methods from math_operations.py.

  • pytest.ini: Configuration file specifying test paths, markers, and additional options.

Important Concepts

Pytest Fixtures

  • Fixture Definition: conftest.py defines a fixture named calculator_instance, creating a shared Calculator instance for tests.

  • Fixture Sharing: Fixtures in conftest.py can be shared across test files in the same directory and subdirectories.

  • Organization and Reusability: Promotes code organization and reuse by centralizing setup logic, ensuring a consistent testing environment.

Pytest Marks and Configurations

  • Custom Mark: @pytest.mark.divide_by_zero categorizes the test_calculator_divide_by_zero function for division by zero scenarios.

  • Parametrize: @pytest.mark.parametrize streamlines testing with different inputs, reducing redundancy.

  • pytest.ini Configuration: Registers custom marks, sets testpaths, and includes additional options for test execution.

Command-line Flags You Should Know

  • -k EXPRESSION: Run tests matching the given substring expression.

  • --pdb: Start the interactive Python debugger on errors or KeyboardInterrupt.

  • --last-failed: Rerun only tests that failed in the last run.

  • -v, --verbose: Increase verbosity.

  • --disable-warnings, --disable-pytest-warnings: Disable warnings summary

Deployment Best Practice: Using Pytest with Tox

meme on test

Consider integrating Tox, a virtualenv test automation tool, for comprehensive testing. Tox sets up virtual environments with predefined dependencies and executes configured test commands. This ensures tests run against the installed package, detecting potential packaging issues.

For a basic understanding of tox, check out my beginner’s guide article on tox

Also, here is a link to a repository where I implemented testing with pytest in a tox.ini file.rent predictor

Conclusion

In the vast landscape of software development, testing stands as a guardian, ensuring the quality, performance, and reliability of our creations. Armed with an understanding of tests, Pytest, and best practices, you're now equipped to elevate the quality and reliability of your Python applications through effective testing.

🌟 Share, Comment, and React! If you found this article helpful or need more clarity, your engagement is highly appreciated.

Happy coding! 🐍✨ Don't forget to test!!! 🧪

Happy coding! Don't forget to test!!!

Top comments (0)