Python Zero to Heros

Online Absolute Beginner Python Tutorials 

Every Sunday 2pm (UK time/ BST)

Get this slide deck: https://slides.com/cheukting_ho/python-pytest-mock

Recap

Python objects - int, float, str, list, dict, bool

Control flows - if-else, for loop, while loop

Functions, modeuls, classes and decorators

strings operations and regex with re

pytest basics

 

Any Questions?

Let's learn more about pytest and using mock in pytest

What is fixture?

Fixtures are functions, which will run before each test function to which it is applied. Fixtures are used to feed some data to the tests such as database connections, URLs to test and some sort of input data - https://www.tutorialspoint.com/pytest/pytest_fixtures.htm

import pytest

@pytest.fixture
def smtp():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com")

def test_ehlo(smtp):
    response, msg = smtp.ehlo()
    assert response == 250

Live coding demo

mark xfail / skip

If you know a test will fail for now (and it's 3am) you just wanna mark it fail so you can fix it later. Or if you wanna skip a test temporary... what should you do?

 

Can I save the test for later?

Pytest will execute the xfailed test, but it will not be considered as part failed or passed tests. Details of these tests will not be printed even if the test fails (remember pytest usually prints the failed test details). We can xfail tests using the following marker −

@pytest.mark.xfail

(you can also mark the reason for failing)

 

Skipping a test means that the test will not be executed. We can skip tests using the following marker −

@pytest.mark.skip

 

https://www.tutorialspoint.com/pytest/pytest_xfail_skip_tests.htm

import sys
@pytest.mark.skipif(sys.version_info < (3,3),
                    reason="requires python3.3")
def test_function():
    ...

Live coding demo

Parametrize your test

option 1: using fixture

import pytest
import smtplib

@pytest.fixture(scope="module",
                params=["smtp.gmail.com", "mail.python.org"])
def smtp(request):
    smtp = smtplib.SMTP(request.param)
    def fin():
        print ("finalizing %s" % smtp)
        smtp.close()
    request.addfinalizer(fin)
    return smtp

Parametrize your test

option 2: @pytest.mark.parametrize

import pytest

from datetime import datetime, timedelta

testdata = [
    (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
    (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]


@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
    diff = a - b
    assert diff == expected


@pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])
def test_timedistance_v1(a, b, expected):
    diff = a - b
    assert diff == expected

Skip/xfail with parametrize

It is possible to apply markers like skip and xfail to individual test instances when using parametrize:

import pytest

@pytest.mark.parametrize(("n", "expected"), [
    (1, 2),
    pytest.mark.xfail((1, 0)),
    pytest.mark.xfail(reason="some bug")((1, 3)),
    (2, 3),
    (3, 4),
    (4, 5),
    pytest.mark.skipif("sys.version_info >= (3,0)")((10, 11)),
])
def test_increment(n, expected):
    assert n + 1 == expected

Live coding demo

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

Live coding demo

Next week:
property-based testing

Sunday 2pm (UK time/ BST)

There are also Mid Meet Py every Wednesday 1pm

Testing month in June

Python Pytest Mock

By Cheuk Ting Ho

Python Pytest Mock

  • 1,030