How to Mock Well in Tests

 

Traps and Pitfalls When Using Mock and Pytest

Grab the slides:

slides.com/cheukting_ho/how-to-mock-well-in-tests

Do you write tests?

What is Unit test

Input

Test function

Check if matches expected output

✅ or ❌

Function

Pytest

pytest is a framework that makes building simple and scalable tests easy.

Perfect for unit test, also have other features via fixtures: patching, mocking, parameterize...

 

https://docs.pytest.org/en/latest/getting-started.html

pip install -U pytest

pytest --version

Test functions

import pytest

def serve_beer(age):
  if (age is None) or (age<18):
    return "No beer"
  else:
    return "Have beer"


def test_serve_beer_legal():
  adult = 25
  assert serve_beer(adult) == "Have beer"

def test_serve_beer_illegal():
  child = 10
  assert serve_beer(child) == "No beer"

Test functions

To run it, in the ternimal:

pytest <you_test.py>::<test_your_func>

Let's see it in action!!!

Expected Exceptions

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0
import pytest

def myfunc():
    raise ValueError("Exception 123 raised")

def test_match():
    with pytest.raises(ValueError, match=r".* 123 .*"):
        myfunc()

there is also another Way (xfail)

Making a "unit" for test is not always easy

Monkey patching in Pytest

Monkey patching is replacing a function/method/class by another at runtime, for testing purpses, fixing a bug or otherwise changing behaviour.

https://stackoverflow.com/questions/41701226/what-is-the-difference-between-mocking-and-monkey-patching

It is useful when your test involve some external funtions (e.g. getting a result from the backend with your API call)

monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=False)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
from pathlib import Path


def getssh():
    """Simple function to return expanded homedir ssh path."""
    return Path.home() / ".ssh"


def test_getssh(monkeypatch):
    # mocked return function to replace Path.home
    # always return '/abc'
    def mockreturn():
        return Path("/abc")

    # Application of the monkeypatch to replace Path.home
    # with the behavior of mockreturn defined above.
    monkeypatch.setattr(Path, "home", mockreturn)

    # Calling getssh() will use mockreturn in place of Path.home
    # for this test with the monkeypatch.
    x = getssh()
    assert x == Path("/abc/.ssh")

Note that in the example, you have to write your own mockreturn function. With a more complex object (requests library objs), it could be quite complicated to create a mock object yourself. 

 

Using Mock

 

The mock library gives you an object you can use to monkey-patch. The mock object from the mock library also gives you excellent features out of the box that helps you test that the mock behaves a certain way.

 

https://docs.python.org/3/library/unittest.mock.html#module-unittest.mock

from unittest import mock
import requests
from requests.exceptions import HTTPError

@mock.patch('requests.get')
def test_google_query(self, mock_get):
  """test google query method"""
  mock_resp = self._mock_response(content="ELEPHANTS")
  mock_get.return_value = mock_resp

  result = google_query('elephants')
  self.assertEqual(result, 'ELEPHANTS')
  self.assertTrue(mock_resp.raise_for_status.called)

Mock.patch Decorators

 

Note: With patch() it matters that you patch objects in the namespace where they are looked up. This is normally straightforward, but for a quick guide read where to patch.

 

https://docs.python.org/3/library/unittest.mock-examples.html#patch-decorators

⚠️

Take away

  1. Invest in testing
     
  2. Find the right tool for testing
     
  3. Share your experience in testing and help others

How to Mock Well in Tests

By Cheuk Ting Ho

How to Mock Well in Tests

  • 1,206