- Python 3
- pipenv (for managing Python environments)
Run make check-prerequisites
to check if the prerequisites are installed.
Run make install-packages
to initialize the python project with dependencies. This will create a Pipfile and install the following python packages in the virtual environment:
- nose2 (for running unit tests)
- nose2-cov (for generating coverage reports)
- RED -> write a test that fails.
- GREEN -> implement the test-supporting functionality to pass the test.
- REFACTOR -> improve the production code AND the tests to absolute perfection.
For the purpose of demonstrating TDD, we will develop a simple calculator app. The calculator app will have an add()
function that takes two arguments and returns the sum of the arguments.
- The calculator should have a
add()
function that takes two arguments and returns the sum of the arguments. - The
add()
function should return an integer. - The
add()
function should validate the arguments and raise aValueError
if the arguments are not numbers.
- Create a test file for the module you want to test. For example, if you want to test the
calculator.py
module, create atest_calculator.py
file intests/
directory. - Write a test that fails. For example, if you want to test the
add()
function insrc/calculator.py
, write a test that callsadd()
with some arguments and assert that the result is what you expect. This is the RED step.
from src.calculator import add
def test_add():
result = add(1, 2)
print(f"\n\nResult from add function --> {result}\n")
assert result == 3
This will fail because the add()
function is not implemented yet. Run make test
to run the test.
- Implement the test-supporting functionality to pass the test. For example, implement the
add()
function incalculator.py
to return the sum of the arguments. This is the GREEN step.
def add(a, b) -> int:
return a + b
This will pass the test because the add()
function now returns the sum of the arguments.
- Improve the production code AND the tests to absolute perfection. For example, refactor the
add()
function to use thesum()
function from theoperator
module. This is the REFACTOR step.
def add(a, b) -> int:
return sum([a, b])
- Validate the arguments and raise a
ValueError
if the arguments are not numbers. For example, add a test that callsadd()
with non-numeric arguments and assert that aValueError
is raised. This is the RED step.
from nose2.tools.such import helper
def test_add_raise_value_error_if_non_integers():
with helper.assertRaises(ValueError):
add("2", 5)
This will fail because the add()
function does not validate the arguments.
- Implement the test-supporting functionality to pass the test. For example, implement the
add()
function to validate the arguments and raise aValueError
if the arguments are not numbers. This is the GREEN step.
def add(a, b) -> int:
if not isinstance(a, int) or not isinstance(b, int):
raise ValueError("Arguments must be integers.")
return sum([a, b])
This will pass the test because the add()
function now validates the arguments and raises a ValueError
if the arguments are not numbers.
Run make test-with-coverage
to run the tests and generate a coverage report. The output will also show missing coverage lines in the source code.
The coverage report will be generated in htmlcov/
directory. Open htmlcov/index.html
in a browser or by using a VS Code extension to view the coverage report.
Sometimes you want to test that a function calls another function. For example, you want to test that some_other_function()
calls function_that_does_some_calc()
with the correct arguments. You can use the mocker
fixture to mock the function_that_does_some_calc()
function and assert that it is called with the correct arguments.
def function_that_does_some_calc(a, b):
return a + b
def some_other_function(a, b):
function_that_does_some_calc(a, b)
return a * b
In this first test, when we test some_other function()
, it calls the real function_that_does_some_calc()
function as its not mocked. But the test coverage report will show that the function_that_does_some_calc()
function has test coverage which is not true as we have not tested it.
def test_some_other_function():
result = some_other_function(2, 3)
assert result == 6
To fix this, we can use the patch
to mock the function_that_does_some_calc()
function.
from unittest.mock import patch
@patch("src.calculator.function_that_does_some_calc")
def test_some_other_function(mock_function_that_does_some_calc):
result = some_other_function(2, 3)
assert result == 6
Now the test coverage report will show that the function_that_does_some_calc()
function has no test coverage which is true as we have mocked it.
Finally, we can test function_that_does_some_calc()
function,
def test_function_that_does_some_calc():
result = function_that_does_some_calc(1, 2)
assert result == 3