TDD for Python with unittest – A Tutorial

Python

Introduction

Test-driven development (TDD) is an approach where you write tests before writing your actual code. The idea behind TDD is to ensure that all the units in your application are working as expected, and it helps in maintaining high quality code. In Python, we can use a testing framework like unittest or pytest for this purpose. unittest is a built-in module in Python.

Table of Contents

What is Test-Driven Development?

Test-driven development (TDD) is an agile software development process that relies on the repetition of a very short development cycle: first the developer writes an automated test case that fails, then the code is written to pass the test, and finally the tests are refactored to improve the design.

First, write the Tests for the Calculator Class

We can use the unittest module to write tests for our future but not yet existing calculator class.

Here is an example of what methods we expect in the class. Furthermore, which outputs we expect when calling them.


import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_addition(self):
        result = self.calc.add(5, 7)
        self.assertEqual(result, 12)

    def test_subtraction(self):
        result = self.calc.subtract(10, 3)
        self.assertEqual(result, 7)

    def test_multiplication(self):
        result = self.calc.multiply(4, 5)
        self.assertEqual(result, 20)

    def test_division(self):
        result = self.calc.divide(16, 4)
        self.assertEqual(result, 4)

if __name__ == '__main__':
    unittest.main()

Second, run the Tests and implement the Test Cases

To run these tests, you would save them in a file (e.g., test_calculator.py), and then use the command line to navigate to that directory and follow the next steps.

Step 1 – Create the Module

Run: python test_calculator.py

This will automatically discover and run all tests within your TestCalculator class, providing you with a report of which tests passed and which failed.

Output:

Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 2, in <module>
    from calculator import Calculator
ModuleNotFoundError: No module named 'calculator'

This means that the module calculator does not exist. Which is correct. That means first we create a file calculator.py. And run the test again.

Step 2 – Create the Class

Run: python test_calculator.py

Output:

Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 2, in <module>
    from calculator import Calculator
ImportError: cannot import name 'Calculator' from 'calculator' (/home/user/projects/blog/testing/calculator.py)

This means that the calculator module now exists. That is correct. But the module does not contain the class Calculator. Now you have to insert the class Calculator into the file calculator.py.


class Calculator:
    pass

Step 3 – Implement the necessary functions

Run: python test_calculator.py

Output:

======================================================================
ERROR: test_addition (__main__.TestCalculator.test_addition)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 9, in test_addition
    result = self.calc.add(5, 7)
             ^^^^^^^^^^^^^
AttributeError: 'Calculator' object has no attribute 'add'

======================================================================
ERROR: test_division (__main__.TestCalculator.test_division)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 21, in test_division
    result = self.calc.divide(16, 4)
             ^^^^^^^^^^^^^^^^
AttributeError: 'Calculator' object has no attribute 'divide'

======================================================================
ERROR: test_multiplication (__main__.TestCalculator.test_multiplication)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 17, in test_multiplication
    result = self.calc.multiply(4, 5)
             ^^^^^^^^^^^^^^^^^^
AttributeError: 'Calculator' object has no attribute 'multiply'

======================================================================
ERROR: test_subtraction (__main__.TestCalculator.test_subtraction)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/projects/blog/testing/test_calculator.py", line 13, in test_subtraction
    result = self.calc.subtract(10, 3)
             ^^^^^^^^^^^^^^^^^^
AttributeError: 'Calculator' object has no attribute 'subtract'

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (errors=4)

Now the class and the module are there but all tested functions are missing. Therefore all 4 tests fail. Create all functions now.


class Calculator:
    def add(self, x, y):
        return x + y

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

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

    def divide(self, x, y):
        if y != 0:
            return x / y
        else:
            raise ValueError("Cannot divide by zero")

Step 4 – Check the result

The implementation now meets all the requirements of the unit test. The same procedure is used for python TDD programming. This ensures that the code has a very high test coverage.

Run: python test_calculator.py

Output:

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Now all requirements from the tests are covered.

Conclusion

TDD is not just about writing code that passes the tests; it’s about writing code that is easy to understand, maintain, and extend. By focusing on testing first in Python, we can ensure our code behaves as expected and helps us avoid bugs in the future. This approach also encourages a clean separation of concerns, making our code easier to read and understand.

To top