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.