Mocking

Mocking objects is a common strategy that developers use to test code that depends on an external system or external resources for it to work properly. This module shows how to use mocking to modify an application server so that it is easier to test.

from collections import Counter
from unittest.mock import MagicMock, PropertyMock, patch

# Module-level constants
_COUNTER = Counter(pid=1)
_START_SUCCESS = "success"
_START_FAILURE = "failure"
_PROTOCOL_HTTP = "http"
_PROTOCOL_HTTPS = "https"
_FAKE_BASE_URL = f"{_PROTOCOL_HTTPS}://www.google.com:443"
_FAKE_PID = 127


class AppServer:
    """Application server.

    Normally we don't mock an application server because it is the runtime
    environment (AKA central nervous system) for business logic, database
    endpoints, network sockets and more. However, this server definition
    is lightweight, so it's okay to mock this.
    """

    def __init__(self, host, port, proto):
        self._host = host
        self._port = port
        self._proto = proto
        self._pid = -1

    @property
    def endpoint(self):
        """Get application server endpoint URL."""
        return f"{self._proto}://{self._host}:{self._port}"

    @property
    def pid(self):
        """Get application server process ID."""
        return self._pid

    @property
    def started(self):
        """Check if application server is started."""
        return self.pid > 0

    def start(self):
        """Start application server."""
        if self.started:
            return _START_FAILURE
        self._pid = _COUNTER["pid"]
        _COUNTER["pid"] += 1
        return _START_SUCCESS


class FakeServer(AppServer):
    """Subclass parent and fake some routines."""

    @property
    def endpoint(self):
        """Mock output of endpoint URL."""
        return _FAKE_BASE_URL

    @property
    def pid(self):
        """Mock output of process ID."""
        return _FAKE_PID


def main():
    # This is the original class instance and it works as expected
    app_server = AppServer("localhost", 8000, _PROTOCOL_HTTP)
    assert app_server.endpoint == f"{_PROTOCOL_HTTP}://localhost:8000"
    assert app_server.start() == _START_SUCCESS
    assert app_server.started is True
    assert app_server.start() == _START_FAILURE

    # But sometimes we cannot test the finer details of a class because
    # its methods depend on the availability of external resources. This
    # is where mocking comes to the rescue. There are a couple approaches
    # that developers use when it comes to mocking

    # Approach 1: Use a `MagicMock` in place of a real class instance
    mock_server = MagicMock()
    assert isinstance(mock_server, MagicMock)
    assert isinstance(mock_server.start_server(), MagicMock)
    mock_server.start_server.assert_called()
    mock_server.endpoint.assert_not_called()

    # Approach 2: Patch a method in the original class
    with patch.object(AppServer, "endpoint", PropertyMock(return_value=_FAKE_BASE_URL)):
        patch_server = AppServer("localhost", 8080, _PROTOCOL_HTTP)
        assert isinstance(patch_server, AppServer)
        assert patch_server.endpoint == _FAKE_BASE_URL
        assert patch_server.started is False
        assert patch_server.start() == _START_SUCCESS

    # Approach 3: Create a new class that inherits the original class
    fake_server = FakeServer("localhost", 8080, _PROTOCOL_HTTP)
    assert isinstance(fake_server, AppServer)
    assert fake_server.endpoint == _FAKE_BASE_URL
    assert fake_server.started is True
    assert patch_server.start() == _START_FAILURE


if __name__ == "__main__":
    main()