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