Delete Set public Set private Add tags Delete tags
  Add tag   Cancel
  Delete tag   Cancel
  • • DevOps notes •
  •  
  • AI
  • Tags
  • Login

Mocking/shaare/G_SN4g

  • python
  • python

Mocking Fundamentals

Introduction

  • When unit testing DevOps scripts that interact with external systems, tests can become slow, unreliable, difficult to set up, or even destructive.
  • Mocking replaces these real dependencies with controlled, fake objects so that tests run quickly and deterministically.
  • Python’s built-in unittest.mock module provides tools to create and configure these mock objects and to track interactions.

What is Mocking?

  • Mocking involves creating objects that mimic the behavior of real functions or classes in a controlled environment.
  • When your code calls a mocked object, you can specify what it returns, simulate exceptions, or inspect how it was called.
  • This allows you to isolate the logic under test and avoid side effects from actual external calls.

Using unittest.mock.patch

  • The patch function replaces a target object with a mock in a specified scope, either for the duration of a function (decorator) or within a context block (with).
  • As a decorator, patch injects the mock into the test function’s parameters; as a context manager, it yields the mock within the with block.
  • It’s important to patch the object where it is looked up in the module under test, not necessarily where it is originally defined.

MagicMock and Configuring Mock Objects

  • When you patch an object, you typically receive a MagicMock instance that you can configure.
  • Use mock.return_value to define what the mock will return when called.
  • Use mock.side_effect to simulate an exception being raised by the mock when invoked, to pass different values to be returned by each execution, or to pass a calable to replace the implemented function.
  • Assertion methods like assert_called_with and assert_called_once let you verify interactions with the mock.

Common Mocking Scenarios in DevOps

  • Network API Calls: Mock requests.get or requests.post to simulate successful responses, HTTP errors, or timeouts.
  • Filesystem Operations: Mock functions like open() or os.path.exists() to simulate file presence or content.
  • Subprocess Execution: Mock subprocess.run to avoid running real system commands and control return codes.
  • Time-Dependent Code: Patch time.sleep or mock datetime.now() to remove delays and make time-based tests deterministic.
from unittest.mock import patch, Mock
from pytest_mock import MockerFixture
from dummy_functions import check_file_exists, get_user_data

# Section: Using unittest.mock.patch
def test_check_file_exists_manual_patch() -> None:
    filepath = "/path/to/some/file.txt"

    patcher = patch("dummy_functions.os.path.exists")
    mock_exists = patcher.start()

    mock_exists.return_value = True

    try:
        result = check_file_exists(filepath=filepath)
        mock_exists.assert_called_once_with(filepath)
        assert result is True
    finally:
        patcher.stop()

def test_check_file_exists_context_manager() -> None:
    filepath = "/path/to/some/file.txt"

    with patch("dummy_functions.os.path.exists") as mock_exists:
        mock_exists.return_value = True

        result = check_file_exists(filepath=filepath)
        mock_exists.assert_called_once_with(filepath)
        assert result is True

@patch("dummy_functions.os.path.exists")
def test_check_file_exists_decorator(mock_exists: Mock) -> None:
    filepath = "/path/to/some/file.txt"

    mock_exists.return_value = True

    result = check_file_exists(filepath=filepath)
    mock_exists.assert_called_once_with(filepath)
    assert result is True

def test_check_file_pytest_mocker(mocker: MockerFixture) -> None:
    filepath = "/path/to/some/file.txt"

    mock_exists = mocker.patch("dummy_functions.os.path.exists")
    mock_exists.return_value = True

    result = check_file_exists(filepath=filepath)
    mock_exists.assert_called_once_with(filepath)
    assert result is True

# Section: MagicMock and Configuring Mock Objects
def test_get_user_data_success(mocker: MockerFixture) -> None:
    mock_api_response: dict[str, str | int] = {
        "id": 1,
        "name": "test user",
    }

    mock_get = mocker.patch("dummy_functions.requests.get")
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = mock_api_response

    data = get_user_data(user_id=1)

    mock_get.assert_called_once_with(
        "https://api.example.com/users/1"
    )
    assert data == mock_api_response

Example of dummy_function.py

import requests
import os
import subprocess
from typing import Optional, Any

def get_user_data(user_id: str | int) -> dict[str, str | int]:
    response = requests.get(
        f"https://api.example.com/users/{user_id}"
    )
    print(f"Status code: {response.status_code}")
    response.raise_for_status()
    return response.json()

def check_file_exists(filepath: str | os.PathLike[str]) -> bool:
    return os.path.exists(filepath)

def get_external_ip():
    """Fetches the current external IP from an external service."""
    try:
        response = requests.get(
            "https://api.ipify.org?format=json", timeout=5
        )
        response.raise_for_status()
        return response.json().get("ip")
    except requests.exceptions.RequestException:
        return None

def get_current_user() -> Optional[str]:
    try:
        result = subprocess.run(
            ["whoami"],
            capture_output=True,
            text=True,
            check=True,
            timeout=5,
        )
        return result.stdout.strip()
    except (
        subprocess.CalledProcessError,
        subprocess.TimeoutExpired,
        FileNotFoundError,
    ):
        return None

def fetch_both_endpoints() -> (
    tuple[dict[str, Any], dict[str, Any]]
):
    """
    Fetch data from two endpoints and return their JSON responses as a tuple.
    """
    response2 = requests.get("https://api.example.com/second")
    response2.raise_for_status()
    data2 = response2.json()

    response1 = requests.get("https://api.example.com/first")
    response1.raise_for_status()
    data1 = response1.json()

    return data1, data2

Advanced Mocking Concepts

Using side_effect

  • The side_effect attribute on a mock allows you to control its behavior beyond a single return value.
  • List of values: When side_effect is set to a list, each call to the mock returns the next item in that list, in order.
  • Callable: When side_effect is a function, it is called with the same arguments as the mock, and its return value is used as the mock’s return.
  • Exception: When side_effect is an exception, it will raise that exception when the original function is called.
  • Use a list when you know the sequence and order of calls; use a function when behavior should vary based on arguments.

Choosing between Mock and MagicMock

  • Mock: A simple replacement that only creates attributes when accessed, and raises errors for undefined methods or attributes.
  • MagicMock: Inherits from Mock and implements Python’s magic methods (__len__, __enter__, etc.) by default.
  • Use Mock by default for stubbing external dependencies to catch unintended interactions.
  • Use MagicMock only when mocking objects that require special behavior, such as context managers or iterables.
import subprocess
import pytest
from unittest.mock import MagicMock
from pytest_mock import MockerFixture
from dummy_functions import (
    get_current_user,
    check_file_exists,
    fetch_both_endpoints,
)

# Section: Using side_effect - Exceptions

def test_get_current_user_command_fails(mocker: MockerFixture):
    mock_run = mocker.patch("dummy_functions.subprocess.run")
    mock_run.side_effect = subprocess.CalledProcessError(
        returncode=1, cmd=["whoami"]
    )

    result = get_current_user()

    assert result is None

# Section: Using side_effect - List for Multiple Calls

def test_check_file_exists_side_effect_list(
    mocker: MockerFixture,
):
    mock_exists = mocker.patch(
        "dummy_functions.os.path.exists",
        side_effect=[True, False],
    )

    assert check_file_exists("some/path/one") is True
    assert check_file_exists("some/path/two") is False

    assert mock_exists.call_count == 2

    assert [
        call.args for call in mock_exists.call_args_list
    ] == [("some/path/one",), ("some/path/two",)]

# Section: Using side_effect - Callable for Multiple Calls

def test_fetch_both_endpoints_by_url(mocker: MockerFixture):
    fake_responses: dict[str, MagicMock] = {}

    for url, data in [
        ("https://api.example.com/first", {"first": "data"}),
        ("https://api.example.com/second", {"second": "data"}),
    ]:
        resp = mocker.MagicMock()
        resp.status_code = 200
        resp.json.return_value = data

        fake_responses[url] = resp

    def _fake_get(url: str) -> MagicMock:
        return fake_responses[url]

    mocker.patch(
        "dummy_functions.requests.get", side_effect=_fake_get
    )

    result = fetch_both_endpoints()

    assert result == ({"first": "data"}, {"second": "data"})

# Section: Choosing between Mock and MagicMock

@pytest.mark.xfail(
    reason="Context managers do not work with Mock", strict=True
)
def test_context_manager_with_mock(mocker: MockerFixture):
    fake_cm = mocker.Mock()
    fake_cm.__enter__.return_value = fake_cm
    fake_cm.read.return_value = "file contents"

    mock_open = mocker.patch("builtins.open")
    mock_open.return_value = fake_cm

    with open("somefile.txt") as f:
        contents = f.read()

    mock_open.assert_called_once_with("somefile.txt")
    assert contents == "file contents"

def test_context_manager_with_magicmock(mocker: MockerFixture):
    fake_cm = mocker.MagicMock()
    fake_cm.__enter__.return_value = fake_cm
    fake_cm.read.return_value = "file contents"

    mock_open = mocker.patch("builtins.open")
    mock_open.return_value = fake_cm

    with open("somefile.txt") as f:
        contents = f.read()

    mock_open.assert_called_once_with("somefile.txt")
    assert contents == "file contents"
1 month ago Permalink
cluster icon
  • Logging Anatomy : Python Logging Anatomy Python’s logging module has five core components: Loggers, Log Records, Handlers, Formatters and Filters. Loggers are hierar...
  • Generators : Generators Writing a class-based iterator requires __iter__() and __next__(), plus manual state management and StopIteration handling. Generator fu...
  • Running External Commands with subprocess.run : Running External Commands with subprocess.run DevOps automation often requires invoking existing CLI tools or scripts to leverage their functionality...
  • Fixtures in Pytest : Fixtures in Pytest As tests grow more complex, repeating setup and cleanup steps makes tests harder to read and maintain. Pytest fixtures allow centr...
  • Logging to Files : Logging to Files Basic File Logging with FileHandler Use logging.FileHandler to write log records to a file. mode='a' (append) preserves existing log...


(97)
Filter untagged links
Fold Fold all Expand Expand all Are you sure you want to delete this link? Are you sure you want to delete this tag? The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community