- 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 aValueErrorif 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.pymodule, create atest_calculator.pyfile 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 == 3This 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.pyto return the sum of the arguments. This is the GREEN step.
def add(a, b) -> int:
return a + bThis 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 theoperatormodule. This is the REFACTOR step.
def add(a, b) -> int:
return sum([a, b])- Validate the arguments and raise a
ValueErrorif the arguments are not numbers. For example, add a test that callsadd()with non-numeric arguments and assert that aValueErroris 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 aValueErrorif 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 * bIn 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 == 6To 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 == 6Now 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