w3resource

Testing and Debugging in Python: A Comprehensive Guide

Introduction to Python Testing and Debugging

Testing and debugging are essential parts of the software development process. They help ensure that your code works correctly and efficiently, and they allow you to identify and fix issues early in the development cycle. In this tutorial, we'll cover some basic testing techniques using Python's built-in ‘unittest’ framework and demonstrate debugging techniques using print statements and the ‘pdb’ module.

This tutorial will focus on practical examples, with explanations.

Example 1: Writing Unit tests with unittest

This example demonstrates how to write simple unit tests using Python's unittest framework. Unit tests help validate individual units of code, such as functions or methods, by testing their behavior against expected outcomes.

Code:

import unittest
# Function to be tested
def add(x, y):
    return x + y

def subtract(x, y):
    return x - y
# Create a test case class by inheriting from unittest.TestCase
class TestMathOperations(unittest.TestCase):
    # Test the add function
    def test_add(self):
        self.assertEqual(add(2, 3), 5)  # Test with positive integers
        self.assertEqual(add(-1, 1), 0) # Test with negative and positive
        self.assertEqual(add(0, 0), 0)  # Test with zeros
    # Test the subtract function
    def test_subtract(self):
        self.assertEqual(subtract(5, 3), 2)  # Test with positive integers
        self.assertEqual(subtract(0, 1), -1) # Test subtracting from zero
        self.assertEqual(subtract(-1, -1), 0) # Test with negative integers
# Run the tests
if __name__ == '__main__':
    unittest.main()

Output:

Ran 2 tests in 0.001s
OK

Explanation:

  • Test Case Creation: We create a test case class 'TestMathOperations' that inherits from 'unittest.TestCase'.
  • Writing Tests: Each test method starts with 'test_' and 'uses assertEqual()' to compare the actual output of a function with the expected output.
  • Running Tests: 'unittest.main()' runs the tests when the script is executed, providing output on the success or failure of each test.

Example 2: Debugging with Print Statements

This example shows how to use print statements to debug code by tracking variable values and understanding the flow of execution.

Code:

# Function to find the maximum number in a list
def find_max(numbers):
    max_number = numbers[0]
    print(f"Initial max: {max_number}")  # Debug print
    for num in numbers:
        print(f"Comparing {num} with {max_number}")  # Debug print
        if num > max_number:
            max_number = num
            print(f"New max found: {max_number}")  # Debug print
    return max_number

# Example list
numbers = [30, 25, 20, 80, 15, 45]

# Calling the function
max_value = find_max(numbers)
print(f"Maximum value is: {max_value}")  # Final output

Output:

Initial max: 30
Comparing 30 with 30
Comparing 25 with 30
Comparing 20 with 30
Comparing 80 with 30
New max found: 80
Comparing 15 with 80
Comparing 45 with 80
Maximum value is: 80

Explanation:

  • Print Statements: By adding print statements throughout the code, we can observe how variables change and trace the program's flow.
  • Debugging Output: The print outputs help identify the program's logic, such as the updates to 'max_number'.

Example 3: Using 'pdb' for Interactive Debugging

This example demonstrates how to use Python's built-in 'pdb' module for interactive debugging, allowing you to step through code, inspect variables, and control execution.

Code:

import pdb
# Function with a bug
def divide(x, y):
    pdb.set_trace()  # Set a breakpoint
    result = x / y
    return result
# Example usage
try:
    print(divide(100, 0))  # This will raise an exception
except ZeroDivisionError as e:
    print(f"Error: {e}")

Output:

Error: division by zero

Explanation:

  • Set Breakpoint: 'pdb.set_trace()' sets a breakpoint, pausing execution and entering the interactive debugger.
  • Inspecting Variables: In the debugger, you can inspect variables, step through code line by line, and understand where issues arise.
  • Handling Exceptions: In this example, 'ZeroDivisionError' is caught, and an error message is printed.

Example 4: Testing with pytest

'pytest' is a popular testing framework that makes writing tests easier and more readable than 'unittest'. It automatically discovers test files and functions, and provides helpful error messages.

Note: Install pytest: pip install pytest

Running Tests: To run the tests, execute 'pytest' in the command line within the directory containing test_calculations.py (pytest test_calculations.py).

Code:

# File: test_calculations.py
# Function to be tested
def multiply(x, y):
    return x * y

# Test function
def test_multiply():
    assert multiply(20, 30) == 600  # Check if the multiplication is correct
    assert multiply(-12, 2) == -24  # Test with negative numbers
    assert multiply(0, 100) == 10  # Test with zero

Output:

_______________________ test_multiply ____________________________
(base) C:\Users\ME>pytest test_calculations.py
    def test_multiply():
        assert multiply(20, 30) == 600  # Check if the multiplication is correct
        assert multiply(-12, 2) == -24  # Test with negative numbers
>       assert multiply(0, 100) == 10  # Test with zero
E       assert 0 == 10
E        +  where 0 = multiply(0, 100)

test_calculations.py:10: AssertionError
================ short test summary info ============= 
FAILED test_calculations.py::test_multiply - assert 0 == 10
=============== 1 failed in 0.12s ===================

Explanation:

Test Discovery: 'pytest' automatically finds tests based on filenames and function names prefixed with 'test_'.

Assertions: 'pytest' uses simple 'assert' statements for testing, providing clear failure messages.

Example 5: Mocking with unittest.mock

Mocking allows you to replace parts of your system under test with mock objects and make assertions about how they have been used. This is useful when testing functions that depend on external systems or APIs.

Code:

from unittest.mock import MagicMock
# Example function that calls an external API
def fetch_data(api_client):
    response = api_client.get("https://example.com/data")
    return response.json()
# Mocking the API client
def test_fetch_data():
    mock_api_client = MagicMock()
    mock_api_client.get.return_value.json.return_value = {"key": "value"}  # Mock response
    # Call the function with the mocked API client
    result = fetch_data(mock_api_client)
    # Assert the expected outcome
    assert result == {"key": "value"}  # Check if the mocked function returns the correct value
    print("Test passed: fetch_data returned the expected result.")  # Output message
    # Verify that the API was called with the correct URL
    mock_api_client.get.assert_called_once_with("https://example.com/data")
    print("Test passed: API call was made with the expected URL.")  # Output message
# Run the test function
test_fetch_data()

Output:

Test passed: fetch_data returned the expected result.
Test passed: API call was made with the expected URL.

Explanation:

  • Imports MagicMock:
    • Imports the 'MagicMock' class from the 'unittest.mock' module to create mock objects that simulate the behavior of real objects for testing purposes.
  • Defines 'fetch_data' Function:
    • A function named 'fetch_data' takes an API client as an argument.
    • It makes a GET request to https://example.com/data using the API client.
    • Returns the JSON response from the API.
  • Defines 'test_fetch_data' Function:
    • A test function named 'test_fetch_data' is defined to test 'fetch_data'.
  • Creates a Mock API Client:
    • A mock API client is created using 'MagicMock()'.
    • Configures the mock client to return a JSON object {"key": "value"} when its get method is called.
  • Calls 'fetch_data' with the Mock Client:
    • Calls 'fetch_data' with the mocked API client and stores the result.
  • Asserts the Expected Result:
    • Checks if the result from 'fetch_data' is {"key": "value"} using an assertion.
    • If the assertion passes, prints "Test passed: 'fetch_data' returned the expected result."
  • Verifies the Correct API Call:
    • Uses 'assert_called_once_with' to ensure the mock API client's get method was called exactly once with the URL https://example.com/data.
    • If the assertion passes, prints "Test passed: API call was made with the expected URL."
  • Runs the Test Function:
    • Calls 'test_fetch_data' to execute the test and print the success messages if all assertions pass.

Example 6: Using assert Statements for Basic Testing

Using 'assert' statements is a quick way to test code in development without a formal testing framework. This is useful for small scripts or during the initial stages of development.

Code:

# Function to test if a number is even
def is_even(n):
    return n % 2 == 0

# Basic tests with assert statements
assert is_even(4) == True  # Test with an even number
assert is_even(3) == False # Test with an odd number
assert is_even(0) == True  # Test with zero
print("All tests passed.")

Output:

All tests passed.

Explanation:

  • Quick Testing: 'assert' is used to verify that a condition is true. If not, it raises an 'AssertionError'.
  • Immediate Feedback: This approach provides immediate feedback on code correctness during development.


Become a Patron!

Follow us on Facebook and Twitter for latest update.