Mocking in PyTest (MonkeyPatching)

This blog post is a continuation of my last post regarding using PyTest and PyTest-Sanic plugin to test Sanic endpoints. Please skim through so that you are brought up to speed with the details of this post.

The main focus on this post is to learn how to mock a module that your test depends on. Mocking is a technique in unit testing. While the way mocking is done is the same across different tools and languages, there are some differences in methods and syntax used.

I will be using Python and PyTest to illustrate how this is done.

Here’s a scenario I have been presented with:

  • I have a Sanic Web Service that accepts POST requests passing in a file path
  • The web service will parse the path provided and instantiate an object that will return a list of files in the path. In doing so, it will use Boto3 to make calls to AWS infrastructure.
  • The web service will return data to the client via JSON object {‘files’: []} with a list of file names of files found inside the array.

The question is this, how can I test the functionality of this endpoint without having the object doing the parsing inside the web service making actual calls to AWS?

Why is making actual calls to AWS a bad idea?

Before we continue answering our question, I want to do a quick intermission to answer a big question you might have. Why not just let it call AWS and return results? How do you know that things work if you don’t actually make the calls?

Great question! The problem is that while AWS is a very reputable company for cloud services, there is no guarantee that they will be 100% available at all times. There is still inherent risk that the infrastructure may go down even if it is just 0.000001%. Another issue is making calls over the Internet is expensive. What I mean by this is it consumes your network, and the most valuable resource, your time. Allow me to exaggerate a bit to demonstrate my point; If you have 10000 test cases written for your application and each test depends on an AWS call, your tests have just consumed 10,000 calls using data and your/your company’s network to make those calls. Some services such as AWS API Gateway charge per request or have an allotted number of requests available to you. Now picture having multiple applications each with 10,000 tests that makes calls to AWS. This gets expensive really fast! Having tests run locally and immediately inform you if your application is behaving properly is paramount. A test that would’ve taken 500ms or even 1 second can now be done in just 6ms or less! That’s at least 83x faster!

Great! So, what is mocking?

I like to use the analogy of a parrot when explaining mocking. Just like a parrot mimicking human speech, mocking in unit testing is mimicking the response that you would expect from a module or a function.

We are swapping out the calls to AWS with the expected response that we would get from AWS depending on the scenario we are trying to test. This is great because we can focus on testing the implementation of our logic rather than dependencies such as network connectivity or the availability of AWS services.

How do we do it?

In your test file

  1. Import your Sanic application
  2. Import your module
  3. Create a class that will represent your response object
  4. Write a method method inside the class that will return the response. Declare it as a static method.
  5. Write your test function, accepting monkeypatch as a parameter
  6. Create a sub-function under your test function that will create an instance of your response class and invoke the response method.
  7. Apply monkeypatching to the module you want to mock, specifying the method to mock and pass the name of your sub-function that will return the response.
  8. Invoke Sanic app’s test_client and make a POST request to the desired endpoint.

Ok, that was a mouthful. Let’s see it in action.

# test_myApp.py
from myApp import app
from myModule import Module
import json

# This class captures the mocked responses of 'Module'
class ModuleResponse:
  @staticmethod
  def filesFound(*arg, **kwargs):
    return ['folder1/file1.pdf', 'folder1/file2.pdf']

  @staticmethod
  def noFilesFound(*arg, **kwargs):
    return []

def test_return_results(monkeypatch):
  async def mock_response(*args, **kwargs):
    return Module().filesFound()

  # 'searchFiles' is a method of Module that the endpoint will call
  # and also the method we want to mock. The 3rd argument passes
  # the function to return the mock results we want
  monkeypatch.setattr(Module, "searchFiles", mock_response)

  # Using Sanic app's test_client to make a post request to our endpoint
  response = app.test_client.post('/search', False, json={'path': 'folder1'})

  result = json.loads(response.body)
  assert response.status == 200
  assert result == {'files': ['folder1/file1.pdf', 'folder/file2.pdf']}

# Providing an empty path should return [] with 200
def test_search_empty_path(monkeypatch):
  async def mock_response(*args, **kwargs):
    return Module().noFilesFound()

  monkeypatch.setattr(Module, "searchFiles", mock_response)

  response = app.test_client.post('/search', False, json={'path': ''})
  result = json.loads(response.body)
  assert response.status == 200
  assert result == {'files': []}

# myModule.py
class Module:
  def __init__(self, searchPath=''):
    self.searchPath = searchPath
    self.files = []
    self.client = boto3.client(
        's3',
        verify=False
    )

  async def getFiles(self):
    return await self.__lookupFile()

  async def __lookupFile():
    ...

# myApp 

@app.route("/search", methods=['POST', 'OPTIONS'])
async def do_post(request):
  ...
  try:
    myModule = Module(searchPath)
    results = await Module.searchFiles()
  except Exception as e:
    return json({'error': e}, status=500)

  myResponse = {
    'files': results
  }
myResponse['files'] = results

  return json(myResponse)

As you can see, monkey patching is key! Monkey Patch replaces our implementation of Module’s searchFiles() method with our fake response.

I hope this guide has helped you gain more understanding with mocking and how to do it using PyTest.

If you think this has helped you in anyway, please help share this post and feel free to add me on Twitter @AlexLHWang.

Sanic Endpoint Testing using PyTest

Recently, I started working on my very first Python web framework at work called Sanic. While the framework is relatively easy to use, I could not say the same for unit testing as its documentation isn’t very clear to me.

If you’re having issues getting unit testing setup for this web framework, you have come to the right place.

Before We Begin

I want to put a disclaimer that I am using Sanic version 19.9.0 on Python 3.7.6 on MacOS 10.15.3; This implies that installation instructions will be biased towards Macs, but Windows or Linux installation of software should be similar with minor modifications.

What is Sanic?

Sanic is a python 3.6+ based web framework that allows you to create an HTTP server. Its strength lies in the use of Python’s new async and await syntax released in version 3.5 allowing non-blocking code for higher performance.

Things you need

  1. Python 3.6+ and pip3 installed on your machine
  2. Virtual Env
  3. PyTest

Setting up the environment

The workflow are as follows

  1. Install Python 3.6 or higher
  2. Install Virtual Env
  3. Install PyTest

Install Python 3.6+

The standard way of doing so would be downloading the latest version from python.org. However, if you use a Mac, you can install homebrew to get the job done.

At the time of writing, the latest version of Python is 3.8.1. To install using homebrew, execute brew install python3. Homebrew will automatically install the latest Python 3 available.

Install Virtual Env

Virtual Env allows you to lock down your dependencies to a specific version and is isolated from your main system. This ensures system packages do not conflict with your project’s packages.

  1. Execute pip3 install virtualenv in terminal.
  2. Create virtual environment virtualenv (ie: virtualenv myProject)
  3. Start the virtual environment source /bin/activate (ie: source myProject/bin/activate)
  4. Install dependencies using pip3 as usual.

To exit from virtual environment use deactivate command.

Install PyTest

  1. In the activated virtual environment, execute pip3 install pytest
  2. execute deactivate && source /bin/activate. This will refresh the virtual environment once the path for PyTest has been set.

Create the First Route of your Application

Example:

# myApp.py
from sanic import Sanic
from sanic.response import text

app = Sanic('myApplication')

@app.route("/")
async def main_route(request):
  return text('Hello')

if __name__ == "__main__":
  app.run(host="0.0.0.0", port=1234)

Creating a Simple Test

The biggest issue I had was trying to understand importing my web service application into Sanic test.

I placed my test at the root directory of my project folder co-locating it with myApp.py just for proof of concept. In the future, I intend to put them under a “test” folder.

PyTest first look at any arguments passed on the command line when executing pytest, then inside config files for testpaths attribute before looking at the current directory recursively for files named test__*.py and *__test.py.

To create a simple test, I co-located my test next to myApp.py

# test_myApp.py
from myApp import app

def test_default_route():
request, response = app.test_client.get('/')
result = response.body
assert response.status == 200
assert result == 'Hello'

The method I used to import my web service application is via relative imports. When code gets imported into the test file, the code is automatically executed (similar to the way JavaScript behaves). This means an instance of the Sanic application is instantiated in memory. Thus, you can use it immediately to test the endpoint.

Sanic object instances contains a method called test_client which mimicks an HTTP client making calls to an endpoint using HTTP methods. In my example, I used the get method going to the main route. You can of course change the HTTP methods to match your needs.

I hope this has helped you get started in endpoint testing with Sanic. I will be adding subsequent blog posts as I learn more about this topic and learn from best practices.