Testing microservices in isolation is crucial for ensuring individual service reliability before integrating them.

Let’s see this in action with a simple example. Imagine we have two services: user-service and order-service. The order-service depends on user-service to validate user IDs before creating an order.

Here’s a simplified order-service Python code using Flask:

# order_service.py
from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

@app.route('/orders', methods=['POST'])
def create_order():
    data = request.get_json()
    user_id = data.get('user_id')
    item = data.get('item')

    if not user_id or not item:
        return jsonify({"error": "user_id and item are required"}), 400

    # Assume user-service is running on http://localhost:5001
    user_service_url = f"http://localhost:5001/users/{user_id}"
    try:
        response = requests.get(user_service_url)
        response.raise_for_status() # Raise an exception for bad status codes
        user_data = response.json()
        if not user_data.get("exists"):
            return jsonify({"error": f"User {user_id} not found"}), 404
    except requests.exceptions.RequestException as e:
        return jsonify({"error": f"Error communicating with user-service: {e}"}), 500

    # Simulate order creation
    order_id = f"ORD-{hash(item + user_id)}"
    return jsonify({"order_id": order_id, "user_id": user_id, "item": item}), 201

if __name__ == '__main__':
    app.run(port=5000, debug=True)

And a basic user-service (which we’ll mock):

# user_service.py (for reference, not run in isolation test)
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/users/<user_id>', methods=['GET'])
def get_user(user_id):
    # In a real scenario, this would query a database
    if user_id == "user123":
        return jsonify({"exists": True, "user_id": user_id, "name": "Alice"})
    else:
        return jsonify({"exists": False}), 404

if __name__ == '__main__':
    app.run(port=5001, debug=True)

Now, let’s write an isolated test for order-service using pytest and requests_mock. We want to test order-service’s POST /orders endpoint without actually starting user-service.

First, install necessary libraries: pip install pytest requests requests-mock

Create a test file, e.g., test_order_service.py:

# test_order_service.py
import pytest
import json
from order_service import app # Import your Flask app
import requests_mock

@pytest.fixture
def client():
    """Configures Flask app for testing."""
    app.config['TESTING'] = True
    with app.test_client() as client:
        yield client

@pytest.fixture
def mock_user_service():
    """Mocks the user-service requests."""
    with requests_mock.Mocker() as m:
        yield m

def test_create_order_success(client, mock_user_service):
    """Tests successful order creation when user exists."""
    user_id_to_mock = "user123"
    item_to_order = "Laptop"

    # Mock the GET request to user-service
    mock_user_service.get(
        f"http://localhost:5001/users/{user_id_to_mock}",
        json={"exists": True, "user_id": user_id_to_mock, "name": "Alice"}
    )

    # Make the POST request to order-service
    response = client.post("/orders",
                           data=json.dumps({"user_id": user_id_to_mock, "item": item_to_order}),
                           content_type="application/json")

    assert response.status_code == 201
    data = json.loads(response.get_data(as_text=True))
    assert "order_id" in data
    assert data["user_id"] == user_id_to_mock
    assert data["item"] == item_to_order

def test_create_order_user_not_found(client, mock_user_service):
    """Tests order creation when user does not exist."""
    user_id_to_mock = "nonexistent_user"
    item_to_order = "Keyboard"

    # Mock the GET request to user-service, returning user not found
    mock_user_service.get(
        f"http://localhost:5001/users/{user_id_to_mock}",
        json={"exists": False},
        status_code=404 # Simulate the user-service response
    )

    # Make the POST request to order-service
    response = client.post("/orders",
                           data=json.dumps({"user_id": user_id_to_mock, "item": item_to_order}),
                           content_type="application/json")

    assert response.status_code == 404
    data = json.loads(response.get_data(as_text=True))
    assert "error" in data
    assert f"User {user_id_to_mock} not found" in data["error"]

def test_create_order_missing_fields(client, mock_user_service):
    """Tests order creation with missing required fields."""
    # No user_id provided
    response = client.post("/orders",
                           data=json.dumps({"item": "Mouse"}),
                           content_type="application/json")

    assert response.status_code == 400
    data = json.loads(response.get_data(as_text=True))
    assert "error" in data
    assert "user_id and item are required" in data["error"]

    # No item provided
    response = client.post("/orders",
                           data=json.dumps({"user_id": "user123"}),
                           content_type="application/json")

    assert response.status_code == 400
    data = json.loads(response.get_data(as_text=True))
    assert "error" in data
    assert "user_id and item are required" in data["error"]

To run these tests, save the order_service.py and test_order_service.py files in the same directory, then execute pytest in your terminal.

The mental model here is that your test for order-service interacts with its Flask application object directly (app.test_client()). When order-service makes an HTTP request to user-service (using requests.get), the requests_mock library intercepts this outgoing request. Instead of the request actually going over the network, requests_mock returns a predefined response that you’ve configured. This allows you to control the behavior of dependencies, simulating success, failure, or specific error conditions from user-service without needing it to be running.

You control the interaction by defining the URL, HTTP method, and the json payload or status_code that requests_mock should return. This isolation is powerful because it decouples your test from the availability and state of other services, making tests faster, more reliable, and easier to debug. You’re essentially creating a "stub" or "mock" for the external dependency.

The core idea is to treat each service as a black box during its own isolation tests. You send it inputs and assert its outputs or side effects. For dependencies, you provide controlled, predictable responses. This is achieved by intercepting network calls your service makes to other services.

A common pitfall is forgetting to mock all external HTTP calls. If your service makes multiple calls to different external services, or even multiple calls to the same service under different conditions, you need to ensure each of those scenarios is explicitly mocked. Failing to mock a call means the request will attempt to go over the network, which will likely fail in a test environment and break your isolated test.

The next logical step in testing is exploring contract testing, where you verify that your service adheres to the agreed-upon communication contract with its dependencies.

Want structured learning?

Take the full Pytest course →