Python provides a mock library that is essential for writing unit tests, but can be intimidating to folks new to mocking.
This is a hands-on guide for those who are new to testing with mocks, also known as 'stubs', 'fakes', and 'test doubles'.
This guide assumes a general knowledge about the Python programming language.
To complete the exercises, you will need the following software installed on your system:
- Python 3
- pip
The commands provided in this guide are targeted to folks using a bash environment (Mac, Linux). You will likely need to perform different steps on a Windows machine.
First, clone this repo
git clone https://github.com/excellalabs/mocking-guide-in-python
cd mocking-guide-in-python
Then install the python dependencies in a virtual environment
python3 -m venv env
. env/bin/activate
pip install -r requirements.txt
A demo web app, written in Flask, provides example integrations with the code we cover in the three exercises. The demo is not required to complete the exercises.
To run the demo:
pip install -r demo/requirements.txt
python -m demo.app
The following link should work in your browser: http://localhost:5000
This repo has three modules with partially written (and failing) tests. Execute the following command to run the failing tests:
pytest
You'll notice that some of the tests take a long time to run. This is because they're hitting an external API that takes a while to respond. Let's fix that first...
Review the code for mocking/bird.py, and the partially completed tests/test_mocking_bird.py.
The tests actually pass... sometimes... try running the tests 10 times, and see how many pass
# Linux/Mac shell command
for i in {1..10}; do pytest tests/test_mocking_bird.py ; done | grep seconds- Create predictable scenarios for interfaces that have unpredictable results
- Open the mock documentation
- Read about library's
patchdecorator - Read about the Mock object's
return_valuecapability
- Read about library's
- Use
patchto mockrandomintin the two tests- In the first test, ensure
sing()always returns "chirp chirp" - In the second test, ensure
sing()always throws an error
- In the first test, ensure
- You're done when the following command results in two passing tests every time:
# Linux/Mac shell command
for i in {1..10}; do pytest tests/test_mocking_bird.py ; done | grep secondsReview the code for mocking/me.py, and the partially completed tests/test_mocking_me.py.
Notice the code for via_gif hits an external API. How do we test that the function handles response codes correctly? Do the tests run quickly?
Unit tests should test the smallest testable part of an application. Tests that hit outside code or APIs are integration tests, not unit tests.
Mocking is a mechanism to decouple outside dependencies, making it easy to create proper unit tests.
- Run tests even if the API is not available
- Create scenarios that are hard to reproduce with the real interface
- Tests run quickly even if the real interface is slow
- Recall what you learned about
patchandreturn_valuein the first exercise - Use
patchto replace requests'spostfunction with a dummy Mock object- In the first test, get the
via_giffunction to return "gif content"- Hint:
return_valueis pretty flexible, there is no need to create a "response" object to house the_contentelement. You can simply domock_object.return_value._content = 'gif content'
- Hint:
- In the second test, get the
via_giffunction to throw an Exception
- In the first test, get the
- You're done when the following command results in two passing tests:
pytest tests/test_mocking_me.py
Review the code for mocking/jay.py, and the partially completed tests/test_mocking_jay.py.
How do we test if hug() gets called when we expect it to?
- Test interactions with interfaces that have no return value
- Recall what you learned about
patchandreturn_valuein the previous exercises - Read about the Mock class
- attributes like
calledandcall_count - assertion helpers like
assert_called_once_withandassert_has_calls
- attributes like
- Use
patchto mockhugin the two tests - Use Mock's attribute
calledto fix the tests- In the first test, ensure
hugis executed - In the first test, ensure
hugis NOT executed
- In the first test, ensure
- Bonus: Get the tests passing with
assert_called_once_with
- You're done when both tests confirm whether hug() was called or not, and the tests pass
pytest tests/test_mocking_jay.py
At this point, ALL your tests should be passing. Let's confirm by running all tests:
pytest