Examples¶
Mocking datetime.datetime¶
We wanted to start with a simple example to check whether the working days from Monday to Friday are calculated correctly.
First, we import
datetime.datetimeandMock:1from datetime import datetime 2from unittest.mock import Mock
Next, we’ll define two test days:
5monday = datetime(year=2021, month=10, day=11) 6saturday = datetime(year=2021, month=10, day=16)
Now let’s define a method to check for working days, bearing in mind that Python’s datetime library treats Mondays as
0and Sundays as6:9def is_workingday(): 10 today = datetime.today() 11 return 0 <= today.weekday() < 5
Then let’s mock
datetime:14datetime = Mock()
Finally, we test our two mock objects:
17def test_workinngday(): 18 # Mock .today() to return Tuesday 19 datetime.today.return_value = monday 20 # Test Tuesday is a weekday 21 assert is_workingday()
24def test_no_workingday(): 25 # Mock .today() to return Saturday 26 datetime.today.return_value = saturday 27 # Test Saturday is not a weekday 28 assert not is_workingday()
Mocking the CLI¶
When testing the Tasks CLI, we’ll also look at how the CliRunner provided by
Typer helps with testing. Typer offers a testing
interface that allows us to invoke our application without having to resort to
subprocess.run(), as in the brief capsys example.
This is useful because we cannot simulate what runs in a separate process. So,
in tests/cli/conftest.py, we can simply pass our application
cusy.tasks.cli.app and a list of strings representing the command to the
invoke() function of our runner: more specifically, we use
shlex.split(command_string) to parse the commands, for example, list
-o 'veit' into ["list", "-o", "veit"], and can then capture and return
the output.
import shlex
import pytest
from typer.testing import CliRunner
from cusy import tasks
runner = CliRunner()
@pytest.fixture()
def tasks_cli(db_path, monkeypatch, tasks_db):
monkeypatch.setenv("ITEMS_DB_DIR", db_path.as_posix())
def run_cli(command_string):
command_list = shlex.split(command_string)
result = runner.invoke(tasks.cli.app, command_list)
output = result.stdout.rstrip()
return output
return run_cli
We can then simply use this fixture to test, for example, the version in
tests/cli/test_version.py:
from cusy import tasks
def test_version(tasks_cli):
assert tasks_cli("version") == tasks.__version__
See also
Mocking attributes¶
Let’s look at how we can use mocking to ensure that, for example, three-digit
version numbers from tasks.__version__() are also output correctly via the
CLI. To do this, we’ll use mock.patch.object() as a context manager:
from unittest import mock
from cusy import tasks
def test_mock_version(tasks_cli):
with mock.patch.object(tasks, "__version__", "100.0.0"):
assert tasks_cli("version") == tasks.__version__
In our test code, we import tasks. The resulting tasks object is what we
are going to patch. The call to mock.patch.object(), used as a
context manager within a with block,
returns a mock object that is cleaned up after the with block:
In this case, the
__version__attribute oftasksis replaced with"100.0.0"for the duration of thewithblock.We then use
tasks_cli()to run our CLI application with theversioncommand. However, when theversion()method is called, the__version__attribute is not the original string, but the string we replaced it with usingmock.patch.object().
Mocking classes and methods¶
In src/cusy/tasks/cli.py, we have defined config() as follows:
def config():
"""List the path to the Tasks db."""
with tasks_db() as db:
print(db.path())
tasks_db() is a context manager that
returns a tasks.TasksDB object. The returned object is then used as db
to call db.path(). So we need to mock two things here: tasks.TasksDB
and one of its methods, path(). Let’s start with the class:
from unittest import mock
from cusy import tasks
def test_mock_tasksdb(tasks_cli):
with mock.patch.object(tasks, "TasksDB") as MockTasksDB:
mock_db_path = MockTasksDB.return_value.path.return_value = "/foo/"
assert tasks_cli("config") == str(mock_db_path)
Let’s make sure it really works:
$ uv run pytest -v -s tests/cli/test_config.py::test_mock_tasksdb
============================= test session starts ==============================
...
configfile: pyproject.toml
plugins: cov-4.1.0, Faker-19.11.0
collected 1 item
tests/cli/test_config.py::test_mock_tasksdb PASSED
============================== 1 passed in 0.04s ===============================
Great, now we just need to move the database mock into a fixture, as we’ll need it in lots of test methods:
@pytest.fixture()
def mock_tasksdb():
with mock.patch.object(tasks, "TasksDB") as MockTasksDB:
yield MockTasksDB.return_value
This fixture mocks the TasksDB object and returns the return_value, so
that tests can use it to substitute values for things like path:
def test_mock_tasksdb(tasks_cli, mock_tasksdb):
mock_tasksdb.path.return_value = "/foo/"
result = runner.invoke(app, ["config"])
assert result.stdout.rstrip() == "/foo/"
Alternatively, the @mock.patch() decorator can also be used to mock classes or objects. In
the following examples, the output of os.listdir is mocked. For this,
db_path does not need to exist on the file system:
import os
from unittest import mock
@mock.patch("os.listdir", mock.MagicMock(return_value="db_path"))
def test_listdir():
assert "db_path" == os.listdir()
Another option is to define the return value separately:
@mock.patch("os.listdir")
def test_listdir(mock_listdir):
mock_listdir.return_value = "db_path"
assert "db_path" == os.listdir()