Intro to Unit Testing and Test Driven Development in Python using Pytest – Part 5
Unit Testing and Test Driven Development with Pytest
Coding Examples – Outgoing Commands and Mocks
In this final piece of our series on basics of unit testing we get to test outgoing commands and learn how to use mocks to remove dependency on external objects.
Intro to Unit Testing and Test Driven Development in Python using Pytest – Part 5
Unit Testing and Test Driven Development with Pytest
Coding Examples – Outgoing Commands and Mocks
In this final piece of our series on basics of unit testing we get to test outgoing commands and learn how to use mocks to remove dependency on external objects.
Model Further Structure of the Application
Before we proceed further into coding the tests it is a good idea to have a look at how our application might be built further. This will allow us to figure out why it is important to remove references to external objects when we are unit testing and replace them with fake objects – Mocks
Now please consider the diagram above representing information flow inside of our application and think what happens when you send set_target_speed message out to the inverter object. We are getting into the whole vicious circle of further dependencies and the universe may reach its heat death before we get the answer back and notice that our outgoing command worked.
Why to Use Mocks?
- We do not want to test the logic of the external object. Since mock replaces it and does not have any complicated logic itself, the tests that we will make using it are much faster.
- We can use mocks also to replace methods from libraries to which objects we do not have a direct access and cannot replace a reference to them easily. This involves a concept called patching which we do not discuss in the basics of unit testing but will in the advanced part later on. This allows us to replace for example built in python methods like open() or input() or get() calls to web APIs using requests library at run time.
- Mocks provide a plethora of methods useful in testing. One of them is return value which gives you ability to return to your object under test precisely what you want. This enables you to test your object for various scenarios when the collaborating object returns different values.
- All of those things let you chop off all the dependencies making tests fast and isolate your object properly.
Seems like finally we have found a use for the unittest framework! For this moment in time all we are going to use from it is the mock module and from that the Mock class. In our new fixture we just instantiate the Mock object and return it for use in test functions later on.
Test set_target_speed Method
The last method to test is an outgoing command. We want to confirm if it gets sent out with a correct parameter to prove that we are introducing correct change in the external object. For this we will replace the reference to our inverter object in the Fridge class and stick a Mock object in its place. To carry out a test of an outgoing command we have to call it on the Fridge object and then use mock to assert about that call.
Let’s look at the code for this test then.
Testing Methodology
We have created a new instance of the Fridge object inside the test function this time and used our mock_inverter fixture instead of a reference to the inverter. The rest of the parameters of our class get instantiated with the default values set in the class itself.
We have then called the method on the object under test with an argument 1000.
Mock object has a wonderful feature that it will register all methods called on it and store them. We can then access them like attributes and make assertions about them.
Now using Mock’s method: assert_called_once_with we create an assertion about the call. The way we have to do this is to use Mock object and access the method that we want to assert about (like an attribute – using dot) and then call Mock’s assert_called_once_with method with parameters comma separated (if there are more than one).
Mock then carries out the assertion if it was actually called with our set_target_speed method with an argument 1000.
Initial test results
We run our test script and make sure that the new test fails in the first try.
================================== FAILURES ========================================
__________________________________ test_set_target_speed _________________________________
mock_inverter = <Mock id='140530704193968'>
def test_set_target_speed(mock_inverter):
"""Test outgoing command called with correct parameter."""
fridge = Fridge(mock_inverter)
> fridge.set_target_speed(1000)
E AttributeError: 'Fridge' object has no attribute 'set_target_speed'
test_fridge.py:69: AttributeError
============================ 1 failed, 4 passed in 0.04 seconds ============================
And it does since there is no set_target_speed method yet.
Add Set_Target_Speed to Pass the Test
The next step for us is to add code in the Fridge class and make the test pass. See the code below for the details.
set_target_speed requires reference to the external object (_inverter) and calls on it to carry out a change of speed. Since in the test we replaced that reference with a Mock what really will happen in the test now is that we will call the method on the Mock object which will remember this call and the name of the method as one of its attributes.
Final test results (all)
This time we run our test script with the verbose option enabled.
(python362_env) tomasz_kluczkowski@xps15:~/Dev/unit_testing_examples/tests$ pytest test_fridge.py -v
====================== test session starts ======================
platform linux -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- /home/tomasz_kluczkowski/.virtualenvs/python362_env/bin/python
cachedir: .cache
rootdir: /home/tomasz_kluczkowski/Dev/unit_testing_examples/tests, inifile:
collected 5 items
test_fridge.py::test_get_temp PASSED
test_fridge.py::test_get_fan_speed_setting PASSED
test_fridge.py::test_set_temp PASSED
test_fridge.py::test_raises_exception_when_temp_too_low PASSED
test_fridge.py::test_set_target_speed PASSED
====================== 5 passed in 0.02 seconds ======================
The verbose option produces a nice output for us listing every single test and its results.
Conclusions
Congratulations! If you read the whole “Unit Testing the Universe” series you should be able to start your own unit testing adventure quite easily. If you have doubts about any aspects covered, do not hesitate to contact me or write in the comments please.
Stuff we have learned:
- Thankfully not everything has to be tested directly. Through a smart approach of testing through the use of the interface we avoid binding ourselves to a current implementation or writing tests that are completely redundant.
- The clear distinction of type and origin of message helps us to decide whether to test it or not.
- You now know how to get started with Pytest and use a simple Mock object to replace references to your external objects.
- We have also covered Test Driven Development and it should no longer be scary. With time and good problem description / specification you will be able to use TDD with ease.
Thank you for reading and keep an eye on this blog as I am preparing the advanced unit testing post where I will explain patching of 3rd party libraries’ methods, test and fixture parametrisation, setup & tear-down code, returning values from Mock objects etc.
Wonderful tutorial. Thank you so much. Are there any other resources on TDD or unit testing with Python that you would recommend?
Hi,
Thx for your appreciation:).
I would recommend reading Leonardo Giordani’s book and blog:
The blog is here:
http://www.thedigitalcatonline.com
“Clean Architectures in Python” ebook (free but you can donate):
https://leanpub.com/clean-architectures-in-python
He was actually my inspiration.
Apart from that you could watch this video from a programming conference by Sandi Metz on how to approach testing in a reasonable way (do not worry it is not in Python, it does not matter – same rules apply in every language):
https://m.youtube.com/watch?v=URSWYvyc42M
This was actually my other inspiration 🙂
Hope that helps,
Tom