Pattern #1 — Test The Real API

The most simple way to test this is to use the real API.

"""Pattern 1 - Full Integration Test"""
 
from src.file_uploader import upload_file
 
 
def test_upload_file():
    file_name = "sample.txt"
    response = upload_file(file_name)
    assert response["success"] is True
    assert response["name"] == file_name
    assert response["key"] is not None

First, create a simple TXT file in the same folder and call it sample.txt .

Let’s move on to Mocking, which in my opinion is an Anti-Pattern for testing 3rd party API integrations.

Let’s learn what NOT to do.

Pattern 2- Mocking/Patching The Requests Library

The most straightforward way is to Mock the requests library.

"""Pattern 2 - Mock the Request Library"""
 
from unittest.mock import patch, Mock, ANY
from src.file_uploader import upload_file
 
 
def test_upload_file():
    file_name = "sample.txt"
    stub_upload_response = {
        "success": True,
        "link": "https://file.io/TEST",
        "key": "TEST",
        "name": file_name,
    }
 
    with patch("src.file_uploader.requests.post") as mock_post:
        mock_post_response = Mock()
        mock_post_response.status_code = 200
        mock_post_response.json.return_value = stub_upload_response
        mock_post.return_value = mock_post_response
 
        response = upload_file(file_name)
 
        assert response["success"] is True
        assert response["link"] == "https://file.io/TEST"
        assert response["name"] == file_name
        mock_post.assert_called_once_with("https://file.io", files={"file": ANY})

If I may ask you, what’s wrong with this or why is this undesirable?

Let’s say your boss tells you to add more functionality. Perhaps the download_file , update_fileor delete_file functionality.

Cons

  • Tightly Coupled to Implementation

  • Brittle and Hard to Maintain

  • Extra Effort to Patch Every Test: You need to remember to apply @patch or use a context manager in every test that may trigger API calls, increasing test setup requirements.

  • Potential for Mixing Business Logic and I/O Concerns

  • Likely Need for Integration and E2E Tests: Since mocks don’t guarantee that the code will work with the actual API, you often need separate integration and end-to-end tests to validate real API behavior.

As you can see I’m not a big fan of this approach for anything other than very simple code.

So what’s the solution?

Pattern #3 — Build an Adaptor/Wrapper Around The 3rd Party API

An interesting idea I got from Harry Percival’s YouTube video — Stop Using Mocks (for a while) — was the concept of an adaptor.

Harry describes it as a wrapper around the 3rd party API or around the I/O.

The wrapper is built to support your interactions with the 3rd party API and is written in your own words (not based directly on the 3rd party Swagger).

src/file_uploader_adaptor.py

import requests
 
 
class FileIOAdapter:
    API_URL = "https://file.io"
 
    def upload_file(self, file_path):
        with open(file_path, "rb") as file:
            response = requests.post(self.API_URL, files={"file": file})
        return response.json()

How could we test it?

We’ll use mock/patch here as well but instead of mocking the requests.post library we’ll patch the upload_file method of the FileIOAdaptor class.

tests/unit/test_pattern3.py

"""Pattern 3 - Create an Adaptor Class and Mock It"""
 
from unittest import mock
from src.file_uploader_adaptor import FileIOAdapter
 
 
def test_upload_file():
    with mock.patch(
        "src.file_uploader_adaptor.FileIOAdapter.upload_file"
    ) as mock_upload_file:
        mock_upload_file.return_value = {"success": True}
 
        adapter = FileIOAdapter()
        response = adapter.upload_file("sample.txt")
 
        assert response == {"success": True}
        mock_upload_file.assert_called_once_with("sample.txt")

Pattern #4 —Dependency Injection

Next, let’s look at another design pattern — dependency injection.

Instead of having our business logic (upload functionality) baked into the FIleUploader class, how about we pass it as a dependency.

from abc import ABC, abstractmethod
import requests
 
 
class FileUploader(ABC):
 @abstractmethod
    def upload_file(self, file_path: str) -> dict:
        raise NotImplementedError("Method not implemented")
 
 
class FileIOUploader(FileUploader):
    API_URL = "https://file.io"
 
    def upload_file(self, file_path: str) -> dict:
        with open(file_path, "rb") as file:
            response = requests.post(self.API_URL, files={"file": file})
        return response.json()
 
 
def process_file_upload(file_path: str, uploader: FileUploader):
    response = uploader.upload_file(file_path)
    return response
 

Pattern #5 — Use a Fake with Dependency Injection

The last pattern (Dependency Injection) is really good but still uses mocks.

What if you wanted to avoid mocks completely?

You can use a Fake instead.

Fake is a different kind of test double to mocks and patches — which is an in-memory representation of the object you’re trying to replace. That’s my informal definition.

tests/unit/test_pattern5.py

"""Pattern 5: Dependency Injection + Fake - Create a Fake Object and Inject It"""
 
from src.file_uploader_dep_injection import process_file_upload
 
 
class FakeFileIOUploader:
    def __init__(self):
        self.uploaded_files = {}
 
    def upload_file(self, file_path: str) -> dict:
        # Simulate uploading by "saving" the file's path in a dictionary
        self.uploaded_files[file_path] = f"https://file.io/fake-{file_path}"
        return {"success": True, "link": self.uploaded_files[file_path]}
 
 
def test_process_file_upload_with_fake():
    fake_uploader = FakeFileIOUploader()
    result = process_file_upload("test.txt", uploader=fake_uploader)
 
    assert result["success"] is True
    assert result["link"] == "https://file.io/fake-test.txt"
    assert "test.txt" in fake_uploader.uploaded_files
 

Pattern #6 — Use a Sandbox API

Using a Sandbox API is a realistic approach for testing external API interactions.

However, while sandbox testing is safer than hitting production, it’s generally slower than using fakes or mocks and you have to remember to clean up to avoid having tons of messy data in your sandbox.