Intro to Unit Testing and Test Driven Development in Python using Pytest – Part 4
Unit Testing and Test Driven Development with Pytest
Coding Examples – Incoming Commands and Exceptions
In the fourth part of our unit testing series we are introducing Test Driven Development (TDD) to test incoming commands and exceptions.
Intro to Unit Testing and Test Driven Development in Python using Pytest – Part 4
Unit Testing and Test Driven Development with Pytest
Coding Examples – Incoming Commands and Exceptions
In the fourth part of our unit testing series we are introducing Test Driven Development (TDD) to test incoming commands and exceptions.
Introduction to Test Driven Development
Let us demystify the subject of Test Driven Development (TDD). In the simplest explanation TDD is a way of creating code in which the test for the next piece of our program gets written before the actual code for our class. Why would anyone even do such a thing you probably ask now? Well, when I read about it the first time I thought similarly probably to you: “these guys are nuts”. But hear me out and consider the following:
- When coding, how often do you feel that you want to stop adding more and more to your class and just begin test because you enjoy it? Like….ehh…never? When we get into the zone it is kind of hard to interrupt the flow and start testing.
- Test driven development though forces you to break your coding into testing and writing the program in little chunks slowly leading to completion of the goal.
- This results in you having to think about the structure you are trying to build before you start programming.
- Important point to note here is that in TDD when we create our test before the code it MUST initially fail. This is to make sure that we did not create a test that passes on its own (trust me you can :)).
Testing Incoming Commands
We now have to create a test for set_temp method which is an incoming command for our Fridge object. Since this is test driven development we have to think what we are trying to achieve before we code the actual method.
set_temp is a message from the Display PCB. The user is able to change the temperature of operation of our Fridge by pushing a button. Then a message gets sent from Display PCB to the Fridge. We know that a command is designed to change the state of the object. Our object has an instance attribute temp and this is what the method set_temp will attempt to alter.
Therefore our assertion will be about the change of the temp attribute. Let’s write it in Pytest.
Testing Methodology
We are still using the same fixture which provides an access to the Fridge object. It gets instantiated with temp = 10. Now before making an assertion we have to call set_temp method with an argument that will clearly show the change in our object’s state.
After the change has been performed we can assert about the results. Since temp was 10 and we called set_temp with argument 5 the change is easy to observe. The attribute should change its value from 10 to 5 and that is our assertion then.
Initial Test Results
Remembering that in TDD we have to run the test before adding the code to make sure that it fails we check that it indeed is the case.
=================================== FAILURES ===================================
________________________________ test_set_temp _________________________________
loaded_fridge = <fridge.Fridge object at 0x7eff633192b0>
def test_set_temp(loaded_fridge):
"""Test correct setting of the target temperature."""
> loaded_fridge.set_temp(5)
E AttributeError: 'Fridge' object has no attribute 'set_temp'
test_fridge.py:101: AttributeError
====================== 1 failed, 2 passed in 0.04 seconds ======================
Process finished with exit code 0
And the test fails as it should pointing us at the lack of set_temp attribute as the source of the failure.
Add set_temp Method to Pass the Test
We are now ready to add the code in the Fridge object to pass our test.
Confirm the Test Passes with set_temp Added
With the code for set_temp method present in the class we are now able to re-run our test script and see if the test passes now.
Test Results
============================= test session starts ==============================
platform linux -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/tomasz_kluczkowski/Dev/unit_testing_examples/tests, inifile:
collected 3 items
test_fridge.py ...
=========================== 3 passed in 0.02 seconds ===========================
Process finished with exit code 0
And all is good.
Testing Exceptions
A lot of the time we need to inform users of our code about an error that occurred during execution. The exceptions that we create should have a kind matching the error and also be as descriptive as they can to remove any chance of user becoming confused about what really triggered the fault. In Pytest testing the exception is quite easy.
Sticking with TDD technique we have to imagine what is the next piece of logic we want to add to our class. Since this is a beer fridge we do not want anyone to be able to freeze the brew! I suppose that we would have restrictions in the Display PCB object that would prevent the user from ever doing such a nasty thing to our beloved drink.
Someone might be using our class in their code though in a wrong way and trying to use the set_temp without restrictions. We want to stop anyone from ever setting temp below 1 degree (for obvious reasons). Normally here, we would convert temp attribute into a property of Fridge class to make sure that no one and nothing can ever get it below 1 but I want to keep the examples super simple so we will just make a restriction in set_temp only.
We create the test first.
Testing Methodology
To test exceptions using TDD technique in Pytest we have to first decide what kind of an exception we expect our method to raise. Since we are talking about incorrect value for temp attribute we choose ValueError as it is closest to the idea that we are trying to code. We also want it to give user a message informing about what caused the error and assert that this particular message was in exception’s value. This prevents Pytest from registering any other ValueError and passing our test with just that (you can have plenty of other ValueErrors the more code you add, custom message allows to identify them correctly).
To assert about an exception raised in Pytest we use with statement. Then we call pytest.raises method and in the brackets we have to put the exception’s name (ValueError in our case). To store exception’s additional data we use as info (others use e or ex_info). info will hold the attributes of the exception for us.
Now for the test to pass we have to trigger the exception inside the with statement (not before it and not after it). Since we said we do not want anyone to set temp < 1 it is enough to call set_temp on the loaded_fridge with argument -10 to trigger the exception.
Having access to the info object we now assert if it contains the custom message that we expect our set_temp method to raise. We have to convert it to string though first, hence str() method used.
Initial Test Results
We confirm that the test for exception fails in the first run.
==============================================FAILURES=====================
______________________________test_raises_exception_when_temp_too_low______
loaded_fridge = <fridge.Fridge object at 0x7f0c18e07128>
def test_raises_exception_when_temp_too_low(loaded_fridge):
"""Test if method raises an exception when temp set < 1."""
with pytest.raises(ValueError) as info:
> loaded_fridge.set_temp(-10)
E Failed: DID NOT RAISE <class 'ValueError'>
test_fridge.py:60: Failed
==========================================================================
1 failed, 3 passed in 0.03 seconds ========================================
(python362_env) tomasz_kluczkowski@xps15:~/Dev/unit_testing_examples/tests$
The test fails an we are informed that the expected exception was never triggered.
Add Code to set_temp to Raise an Exception
To raise an exception we will add a simple if statement checking value of new_temp argument.
Confirm the Test Passes
We can now re-run the test script to check whether the code for raising an exception we added satisfies the test.
Test Results
============================= test session starts ==============================
platform linux -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/tomasz_kluczkowski/Dev/unit_testing_examples/tests, inifile:
collected 4 items
test_fridge.py ....
=========================== 4 passed in 0.01 seconds ===========================
Process finished with exit code 0
Success!
Summary
We have covered the method of testing using Test Driven Development. Important thing to remember is to always run the test first and confirm it fails before adding the code that will make it pass. This confirms that the test does not pass by accident.
You also saw how to solve the problem of testing incoming commands and exceptions – two important aspects of our code.
In the next article in the “Unit Testing the Universe” series we will be finishing off the basics of unit testing with an example of how to test an outgoing command with the use of a Mock object. ‘Till next time.
0 Comments