-
Notifications
You must be signed in to change notification settings - Fork 2
Test Driven Development
Test-driven development (TDD) is a practice where test cases are developed to specify and validate what the code will do. In other words, the approach consists in writing a test for certain functionality, and then if the test fails then new code is written in order to pass the test and make sure the code is bug-free. This approach instructs the developers to write new code only if the test fails, this method avoids duplication of code.
Below we have a tutorial written in Python, but the same methodology can be implemented in any language with the corresponding libraries.
The process for TDD can be defined in this way:
- Add a test for a certain feature: the test will pass if and only if the feature’s specifications are met.
- Run all the tests. The test should fail because that feature has not been implemented yet.
- Write the simplest needed code to be able to pass the test.
- Run all the tests, the tests should pass now.
- Refactor the code to improve readability and maintenance. After refactoring, run again the tests to be sure all the functionalities are not broken.
- Repeat from step 1.
To implement TDD in Python you can make use of the built-in library pytest
.
You need to install the library first:
python3 -m pip install pytest
pytest
implements a test discovery based on certain rules:
- the tests need to be placed in the folder
tests
- the tests files need to be named as follows:
test_*.py
or*_test.py
- the testing functions need to be called as follows:
test*
- the testing classes need to be called as follows:
Test*
You can then run the test by invoking from the project directory:
pytest
If you need to set something up before running each test, you can include the function setup_function()
and if you need some cleanup code after each test, you can include the function teardown_function()
:
def setup_function():
# will run before each test
pass
def teardown_function():
# will run after each test
pass
If you need to set something up only once in the module, you can include the function setup_module()
and if you need some cleanup code only once in the module, you can include the function teardown_module()
:
def setup_module():
# will run once per module
pass
def teardown_module():
# will run once per module
pass
If your tests are defined in a test class you can define the setup_method
and teardown_method
which will be run before and after each test. Example:
class TestClass:
def setup_method(self):
pass
def teardown_method(self):
pass
def test1(self):
"""a test"""
pass
def test2(self):
"""another test"""
pass
Test-Driven development is more oriented to the software specifications. Once you have defined the specifications of the functionality you want to have you can start writing your first test. For example, you are asked to write a function that has these requirements:
- Define a function called
get_value
which takes a parameter as input. - It returns a string
-
get_value
should return "Hi" when a multiple of 3 is passed an input parameter -
get_value
should return "Bye" when a multiple of 5 is passed an input parameter -
get_value
should return "Hi - Bye" when a multiple of 3 and 5 is passed an input parameter -
get_value
will return "Fine" for all other values of the input_parameter
We can write the tests for those requirements with the following steps:
- Call the function
get_value
- Get "Fine" when I pass any value
- Get "Hi" when I pass a multiple of 3
- Get "Bye" when I pass a multiple of 5
- Get "Hi - Bye" when I pass a multiple of 3 and 5
We implement the first test checking that the function get_value
exists.
def test_we_can_call_get_value():
input_value = 11
output = get_value()
assert output == None
The test will fail with the following error:
NameError: name 'get_value' is not defined
We then need to define a get_value
function
def get_value(value):
pass
This will allow our test to pass.
The second requirement asks us to change the function so that it returns "Fine" whenever a value is passed. We write the corresponding test inside the TestFunctionality
class for this requirement:
def test_call_get_value_returns_Fine():
input_value = 11
output = get_value(input_value)
assert output == "Fine"
The test will fail. We then add the functionality to get_value
, we then need to refactor the code, the test we implemented in the first step needs to be deleted because it will now fail:
def get_value(value):
return "Fine"
We add a test to check that get_value
returns "Hi" when a value multiple of 3 is passed as an input parameter:
def test_call_get_value_returns_Hi_when_mutple_of_3_is_passed():
input_value = 9
output = get_value(input_value)
assert output == "Hi"
The test will of course fails, so we have to modify the function get_value
to make the test pass:
def get_value(value):
if (value % 3) == 0:
return "Hi - Bye"
return "Fine"
We add a test to check that get_value
returns "Bye" when a value multiple of 5 is passed as an input parameter:
def test_call_get_value_returns_Bye_when_mutple_of_5_is_passed():
input_value = 25
output = get_value(input_value)
assert output == "Bye
The test will fail, we then modify get_value
to make the test pass:
def get_value(value):
if (value % 3) == 0:
return "Hi - Bye"
elif (value % 5) == 0:
return "Bye"
return "Fine"
The last step requires us to return "Hi - Bye" when we pass a multiple of 3 and 5 to the function get_value
. We write a test to check this:
def test_call_get_value_returns_ByeandHi_when_mutple_of_5and3_is_passed():
input_value = 15
output = get_value(input_value)
assert output == "Hi - Bye"
The test will fails, we then modify get_value
to make the test pass:
def get_value(value):
if (value % 3) == 0 and (value % 5) == 0:
return "Hi - Bye"
elif (value % 3) == 0:
return "Hi"
elif (value % 5) == 0:
return "Bye"
return "Fine"
Congratulations!!! You successfully implemented your first function with the TDD practice!
Click here for the full code!
def get_value(value):
if (value % 3) == 0 and (value % 5) == 0:
return "Hi - Bye"
elif (value % 3) == 0:
return "Hi"
elif (value % 5) == 0:
return "Bye"
return "Fine"
def test_call_get_value_returns_Fine():
input_value = 11
output = get_value(input_value)
assert output == "Fine"
def test_call_get_value_returns_Hi_when_mutple_of_3_is_passed():
input_value = 9
output = get_value(input_value)
assert output == "Hi"
def test_call_get_value_returns_Bye_when_mutple_of_5_is_passed():
input_value = 25
output = get_value(input_value)
assert output == "Bye"
def test_call_get_value_returns_ByeandHi_when_mutple_of_5and3_is_passed():
input_value = 15
output = get_value(input_value)
assert output == "Hi - Bye"
Composition is your best friend
This means that if your function or class needs some external resource, it is best that it receives it as a parameter. This way, you can test with mock data.
For example, you have a class that reads from a database and does something.
If the database is merged with the class (for example, it is a member of the class):
class ReadFromDBAndDoThings:
self.database = ProductionDatabase()
def doThingsInDatabase(self):
self.database.change_all_the_data() # this is dangerous to try in tests!
You limit yourself, because you can only test with the real database. Not only is this dangerous, but you don't have many options to test.
Using composition, we pass the database to this class as a parameter:
class ReadFromDBAndDoThings:
def __init__(self, database):
self.database = database
def doThingsInDatabase(self):
self.database.change_all_the_data()
Now when you test you can use any fake database you want:
def test_database_is_changed():
fake_database = FakeDatabase()
object_to_test = ReadFromDBAndDoThings(fake_database)
object_to_test.doThingsInDatabase()
And you can test in different configurations and with no fear of breaking something important.