This article is brought with ❤ to you by Semaphore.
Introduction
Mocking is simply the act of replacing the part of the application you are testing with a dummy version of that part called a mock.
Instead of calling the actual implementation, you would call the mock, and then make assertions about what you expect to happen.
What are the benefits of mocking?
Increased speed— Tests that run quickly are extremely beneficial. E.g. if you have a very resource intensive function, a mock of that function would cut down on unnecessary resource usage during testing, therefore reducing test run time.
Avoiding undesired side effects during testing— If you are testing a function which makes calls to an external API, you may not want to make an actual API call every time you run your tests. You'd have to change your code every time that API changes, or there may be some rate limits, but mocking helps you avoid that.
Prerequisites
You will need to have Python 3.3 or higher installed. Get the correct version for your platform here. I will be using version 3.6.0 for this tutorial.
Once you have that installed, set up a virtual environment:
python3 -m venv mocking
Activate the virtual environment by running:
source mocking/bin/activate
After that, add a main.py
file where our code will reside and a test.py
file
for our tests.
touch main.py test.py
Basic Usage
Imagine a simple class:
classCalculator:defsum(self,a,b):returna+b
This class implements one method, sum
that takes two arguments, the
numbers to be added, a
and b
. It returns a + b
;
A simple test case for this could be as follows:
fromunittestimportTestCasefrommainimportCalculatorclassTestCalculator(TestCase):defsetUp(self):self.calc=Calculator()deftest_sum(self):answer=self.calc.sum(2,4)self.assertEqual(answer,6)
You can run this test case using the command:
python -m unittest
You should see output that looks approximately like this:
.
_____________________________________________________________
Ran 1test in 0.003s
OK
Pretty fast, right?
Now, imagine the code looked like this:
importtimeclassCalculator:defsum(self,a,b):time.sleep(10)# long running processreturna+b
Since this is a simple example, we are using time.sleep()
to simulate
a long running process. The previous test case now produces the following
output:
.
_____________________________________________________________
Ran 1test in 10.003s
OK
That process has just considerably slowed down our tests. It is clearly
not a good idea to call the sum
method as is every time we run tests.
This is a situation where we can use mocking to speed up our tests and
avoid an undesired effect at the same time.
Let's refactor the test case so that instead of calling sum
every
time the test runs, we call a mock sum
function with well defined behavior.
fromunittestimportTestCasefromunittest.mockimportpatchclassTestCalculator(TestCase):@patch('main.Calculator.sum',return_value=9)deftest_sum(self,sum):self.assertEqual(sum(2,3),9)
We are importing the patch
decorator from unittest.mock
. It
replaces the actual sum
function with a mock function that behaves
exactly how we want. In this case, our mock function always returns 9.
During the lifetime of our test, the sum
function is replaced with
its mock version. Running this test case, we get this output:
.
_____________________________________________________________
Ran 1test in 0.001s
OK
While this may seem counter-intuitive at first, remember that mocking allows you
to provide a so-called fake
implementation of the part of your system you are
testing. This gives you a lot of flexibility during testing. You'll see how to
provide a custom function to run when your mock is called instead of hard coding
a return value in the section titled Side Effects
.
A More Advanced Example
In this example, we'll be using the requests
library to make API calls. You
can get it via pip install
.
pip install requests
Our code under test in main.py
looks as follows:
importrequestsclassBlog:def__init__(self,name):self.name=namedefposts(self):response=requests.get("https://jsonplaceholder.typicode.com/posts")returnresponse.json()def__repr__(self):return'<Blog: {}>'.format(self.name)
This code defines a class Blog
with a posts
method. Invoking posts
on the
Blog object will trigger an API call to jsonplaceholder
, a JSON generator API
service.
In our test, we want to mock out the unpredictable API call and only test that a
Blog object's posts method returns posts. We will need to patch
all Blog
objects' posts
methods as follows.
fromunittestimportTestCasefromunittest.mockimportpatch,MockclassTestBlog(TestCase):@patch('main.Blog')deftest_blog_posts(self,MockBlog):blog=MockBlog()blog.posts.return_value=[{'userId':1,'id':1,'title':'Test Title','body':'Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy\ lies a small unregarded yellow sun.'}]response=blog.posts()self.assertIsNotNone(response)self.assertIsInstance(response[0],dict)
You can see from the code snippet that the test_blog_posts
function is
decorated with the @patch
decorator. When a function is decorated using
@patch
, a mock of the class, method or function passed as the target to
@patch
is returned and passed as an argument to the decorated function.
In this case, @patch
is called with the target main.Blog
and returns a
Mock which is passed to the test function as MockBlog
. It is important to
note that the target passed to @patch
should be importable in the environment
@patch
is being invoked from. In our case, an import of the form
from main import Blog
should be resolvable without errors.
Also, note that MockBlog
is a variable name to represent the created mock and
can be you can name it however you want.
Calling blog.posts()
on our mock blog object returns our predefined JSON.
Running the tests should pass.
.
_____________________________________________________________
Ran 1test in 0.001s
OK
Note that testing the mocked value instead of an actual blog object allows us to make extra assertions about how the mock was used.
For example, a mock allows us to test how many times it was called, the arguments it was called with and even whether the mock was called at all. We'll see additional examples in the next section.
Other Assertions We Can Make on Mocks
Using the previous example, we can make some more useful assertions on our Mock blog object.
importmainfromunittestimportTestCasefromunittest.mockimportpatchclassTestBlog(TestCase):@patch('main.Blog')deftest_blog_posts(self,MockBlog):blog=MockBlog()blog.posts.return_value=[{'userId':1,'id':1,'title':'Test Title,'body':'Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy\ lies a small unregarded yellow sun.'}]response=blog.posts()self.assertIsNotNone(response)self.assertIsInstance(response[0],dict)# Additional assertionsassertMockBlogismain.Blog# The mock is equivalent to the originalassertMockBlog.called# The mock wasP calledblog.posts.assert_called_with()# We called the posts method with no argumentsblog.posts.assert_called_once_with()# We called the posts method once with no arguments# blog.posts.assert_called_with(1, 2, 3) - This assertion is False and will fail since we called blog.posts with no argumentsblog.reset_mock()# Reset the mock objectblog.posts.assert_not_called()# After resetting, posts has not been called.
As stated earlier, the mock object allows us to test how it was used by checking the way it was called and which arguments were passed, not just the return value.
Mock objects can also be reset to a pristine state i.e. the mock object has not been called yet. This is especially useful when you want to make multiple calls to your mock and want each one to run on a fresh instance of the mock.
Side Effects
These are the things that you want to happen when your mock function is called. Common examples are calling another function or raising exceptions.
Let us revisit our sum
function. What if, instead of hard coding a return
value, we wanted to run a custom sum
function instead? Our custom function
will mock out the undesired long running time.sleep
call and only remain with
the actual summing functionality we want to test. We can simply define a
side_effect
in our test.
fromunittestimportTestCasefromunittest.mockimportpatchdefmock_sum(a,b):# mock sum function without the long running time.sleepreturna+bclassTestCalculator(TestCase):@patch('main.Calculator.sum',side_effect=mock_sum)deftest_sum(self,sum):self.assertEqual(sum(2,3),5)self.assertEqual(sum(7,3),10)
Running the tests should pass:
.
_____________________________________________________________
Ran 1test in 0.001s
OK
Continous Integration Using Semaphore CI
Adding Continous Integration with Semaphore is very easy. Once you have everything committed and pushed to Github or Bitbucket, go here and create a new account or sign into an existing account. We'll be using a Github repo containing the Blog class example and test.
From your dashboard, click Add New Project
.
You will be asked to select either Github or Bitbucket as a source. Pick a source as per your preference.
After selecting a source, select the repository.
Next, select the branch to build from.
Semaphore will analyze the project and show you the build settings:
Customize your plan to look as follows:
After that, click Build with these settings
at the bottom of that page.
Once your build passes, that's it. You have successfully set up continuous integration on Semaphore CI.
Conclusion
In this article, we have gone through the basics of mocking with Python. We have
covered using the @patch
decorator and also how to use side effects to provide
alternative behavior to your mocks. We also covered how to run a build on
Semaphore.
You should now be able to use Python's inbuilt mocking capabilities to replace parts of your system under test to write better and faster tests.
For more detailed information, the official docs are a good place to start.
Please feel free to leave your comments and questions in the comments section below.
This article is brought with ❤ to you by Semaphore.