left-icon

Essential Python Succinctly®
by Ed Freitas

Previous
Chapter

of
A
A
A

CHAPTER 10

Unit Testing and Test-Driven Development


Quick intro

Unit testing and test-driven development (TDD) are critical practices in software engineering that help ensure code reliability and maintainability. In TDD, tests are typically written before the actual code, guiding the development process to meet specified requirements.

Python has powerful testing libraries, including unittest and pytest, which enable the creation of automated tests for your code.

Additionally, understanding test coverage and the use of mocks for testing isolated functions and components is essential for building robust applications.

Note: You can run the following code examples by opening the built-in terminal within VS Code and executing the command: python <script>.py. Replace <script> with the name of the respective Python file to execute.

Writing unit tests (unittest)

Python’s built-in unittest framework provides a straightforward way to write tests. With it, you can define test cases and check conditions using assertions and test suites.

Let’s create a simple Calculator class with basic operations (addition, subtraction, multiplication, and division) and write unit tests for each function.

Code Listing 10-a: calc.py

class Calculator:

    """A simple calculator class for basic arithmetic operations."""

   

    def add(self, a, b):

        return a + b

    def subtract(self, a, b):

        return a - b

    def multiply(self, a, b):

        return a * b

    def divide(self, a, b):

        if b == 0:

            raise ValueError("Cannot divide by zero")

        return a / b

Next, let’s write unit tests for each method in this Calculator class using unittest.

Code Listing 10-b: testcalc.py

import unittest

from calculator import Calculator

class TestCalculator(unittest.TestCase):

   

    def setUp(self):

        """Set up a Calculator instance before each test method."""

        self.calc = Calculator()

    def test_add(self):

        """Test the addition method."""

        self.assertEqual(self.calc.add(2, 3), 5)

        self.assertEqual(self.calc.add(-1, 1), 0)

    def test_subtract(self):

        """Test the subtraction method."""

        self.assertEqual(self.calc.subtract(5, 3), 2)

        self.assertEqual(self.calc.subtract(0, 5), -5)

    def test_multiply(self):

        """Test the multiplication method."""

        self.assertEqual(self.calc.multiply(3, 4), 12)

        self.assertEqual(self.calc.multiply(-2, 3), -6)

    def test_divide(self):

        """Test the division method."""

        self.assertEqual(self.calc.divide(10, 2), 5)

        self.assertRaises(ValueError, self.calc.divide, 10, 0)

if __name__ == "__main__":

    unittest.main()

Explanation:

·     Calculator class:

o     This class includes methods for basic arithmetic operations.

o     divide includes a check for division by zero, raising a ValueError if b is 0.

·     Importing and setting up unittest:

o     import unittest and from calculator import Calculator import the testing framework and the class to be tested.

o     setUp initializes a Calculator instance before each test.

·     Writing test methods:

o     Each method tests a specific operation (test_add, test_subtract, etc.).

o     self.assertEqual checks if the result matches the expected value.

o     self.assertRaises verifies that divide raises a ValueError when dividing by zero.

·     Running the tests:

o     The code checks for correctness across various cases, making it easy to identify errors if any of the tests fail.

Testing with pytest

The pytest library is a popular testing framework that provides simpler syntax, additional functionalities, and plugins for extended capabilities—pytest can run unittest tests, but it has its own syntax that’s often more concise.

The following is the same test for the calculator, but written using pytest syntax.

Code Listing 10-c: testcalcpy.py

import pytest

from calc import Calculator

@pytest.fixture

def calc():

    """Fixture for Calculator instance."""

    return Calculator()

def test_add(calc):

    assert calc.add(2, 3) == 5

    assert calc.add(-1, 1) == 0

def test_subtract(calc):

    assert calc.subtract(5, 3) == 2

    assert calc.subtract(0, 5) == -5

def test_multiply(calc):

    assert calc.multiply(3, 4) == 12

    assert calc.multiply(-2, 3) == -6

def test_divide(calc):

    assert calc.divide(10, 2) == 5

    with pytest.raises(ValueError):

        calc.divide(10, 0)

Explanation:

·     Using fixtures:

o     @pytest.fixture is a fixture that initializes Calculator() before each test, similar to setUp in unittest.

·     Simplified assertions:

o     assert statements replace self.assertEqual, making the tests concise.

o     pytest.raises(ValueError) checks that dividing by zero raises the expected error.

·     Running pytest:

o     Simply run pytest from the terminal to see organized test results.

Test coverage

Test coverage measures how much of your code is covered by tests. Python’s coverage library works well with both unittest and pytest for measuring test coverage.

Code Listing 10-d: Install and Run Test Coverage

# Install coverage

pip install coverage

# Run coverage with pytest

coverage run -m pytest

# Generate a coverage report

coverage report -m

Explanation:

·     Installation: pip install coverage installs the coverage package.

·     Running coverage: coverage run -m pytest executes tests with coverage tracking.

·     Generating reports: coverage report -m shows the percentage of code covered by tests, allowing you to identify untested sections.

Mocking

Sometimes, you must test code interacting with external systems like APIs and databases. Mocking allows you to replace these parts with mock objects to isolate the code being tested.

Let’s expand the Calculator class to include an API call and demonstrate how to mock it in tests.

Code Listing 10-e: calcapi.py

import requests

class Calculator:

   

    def add(self, a, b):

        return a + b

    def get_random_number(self):

        response = requests.get("https://randomapi.com/api/random")

        if response.status_code == 200:

            return response.json()['number']

        else:

            return None

In this example, get_random_number makes a GET request to an API. Here’s how to mock this API call.

Code Listing 10-f: mock.py

import unittest

from unittest.mock import patch

from calcapi import Calculator

class TestCalculatorWithAPI(unittest.TestCase):

   

    @patch("calcapi.get")

    def test_get_random_number(self, mock_get):

        """Mock the API call and test get_random_number."""

        calc = Calculator()

       

        # Define mock response data

        mock_response = mock_get.return_value

        mock_response.status_code = 200

        mock_response.json.return_value = {'number': 42}

        # Test the method with the mocked response

        self.assertEqual(calc.get_random_number(), 42)

if __name__ == "__main__":

    unittest.main()

Explanation:

·     Mocking the API request:

o     @patch("calcapi.get") replaces the requests.get call in Calculator with a mock.

·     Configuring the mock:

o     mock_get.return_value simulates the response object.

o     mock_response.json.return_value = {'number': 42} sets the JSON response.

·     Testing with mocked data:

o     The statement self.assertEqual(calc.get_random_number(), 42) checks that the get_random_number method returns the mocked value.

Recap

Unit testing and TDD play a vital role in building reliable software. Using unittest and pytest, you can create structured test cases, maintain test coverage, and isolate code behavior using mocks.

These tools and techniques allow you to develop high-quality applications that are easier to maintain and scale over time.

Scroll To Top
Disclaimer

DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.