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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s