你是否曾试图测试与外部第三方 API 集成的代码,却不知如何推进?

你一直在使用模拟(Mock)方法,但却不断遭遇错误,不禁怀疑自己是否只是在测试模拟对象,而非真正的集成效果。

虽然对代码进行单元测试相对简单,但处理第三方依赖时,情况就变得棘手了。

外部 API 是应用程序不可或缺的一部分,它们使应用程序能够交换数据并提供更强大的功能。

但这也意味着你有责任确保代码能够可靠地处理响应,即使数据来自你无法控制的服务。

你该如何测试这些交互,以便在问题影响到用户之前就将其发现呢?

你应该连接到真实的 API 还是创建一个可控的环境?使用模拟是正确的方法吗,还是使用沙盒环境或伪对象更好呢?

在本文中,我们将深入探讨测试外部 API 集成的核心原则,并探索 10 种强大的设计模式及其优缺点。

你将了解不同技术的权衡取舍,并探索一些实用的方法,如集成测试、模拟、伪对象、依赖注入,以及像 VCR.py 和 WireMock 这样的工具。

通过这些内容,你将能够根据项目需求选择合适的策略,平衡技能、限制条件、截止日期和团队能力等因素。

软件架构与测试都涉及权衡取舍

在深入探讨这些概念之前,你需要了解一个非常重要的事情。

在软件架构中,每一个决策都涉及权衡,测试外部 API 也不例外。

你需要在可靠性、可维护性和测试速度之间取得平衡。

直接测试实时 API 可以提供真实世界的准确性,但速度较慢,需要网络稳定,而且如果外部 API 意外发生变化,测试结果可能不可靠。

更不用说在时间、金钱和资源方面的成本相当高。

相反,使用模拟或创建适配器可以简化测试,使其更快且更独立,但可能会忽略一些微妙的实际行为,尤其是当 API 不断发展时。

模拟还存在使测试与实现细节紧密耦合的风险,从而使代码重构变得复杂。

最终,选择正确的策略取决于公司的限制条件、API 的复杂性和重要性。

目标是找到一个平衡点:设计出能够适应 API 变化的测试,同时确保它们能反映真实集成的关键方面,且无需过多的维护工作。

在本文中,我们将介绍几种设计模式和反模式,我鼓励你仔细阅读每种模式的优缺点,以便做出最佳决策。因为没有一种方法是绝对正确的。

源代码

在本文中,我们将源码直接写在文中。

使用 requests 库,代码如下:

# src/file_uploader.py
import requests

def upload_file(file_name):
    with open(file_name, "rb") as file:
        response = requests.post("https://file.io", files={"file": file})
        response.raise_for_status()
        upload_data = response.json()

        if response.status_code == 200:
            print(f"File uploaded successfully. Upload data: {upload_data}")
            return upload_data
        else:
            raise Exception("File upload failed.")

这段简单的代码使用 file_name 参数向 REST API 发出 POST 请求,并返回响应。

让我们看看测试代码是什么样的。

模式 1 - 测试真实 API

测试此功能最简单的方法是使用真实的 API。

# tests/unit/test_pattern1.py
"""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

很简单。让我们运行一下。

首先,在同一文件夹中创建一个简单的 TXT 文件,并将其命名为 sample.txt

$ pytest tests/unit/test_pattern1.py

看起来不错,但让我们来了解一下这种方法的权衡之处。

  • 优点
  • 真实测试:能真实反映应用程序在生产环境中的运行情况,包括响应时间、数据结构和错误处理。
  • 全面集成信心:确保应用程序与 API 提供商端的任何更新或更改兼容,这对于关键任务集成尤为有用。
  • 无需模拟:不使用模拟,意味着测试更简单,避免了基于实现细节或对 API 行为的错误假设而创建脆弱测试的风险。
  • 缺点
  • 速度慢且不可靠:受网络延迟和可用性问题的影响,测试速度较慢,且结果可能不稳定。
  • 速率限制和成本:许多 API 实施速率限制或根据使用情况收费,如果频繁运行测试,成本会迅速增加。
  • 非确定性:请求之间的数据或状态变化会使测试不可重复,且更难调试。

让我们继续讨论模拟,在我看来,模拟是测试第三方 API 集成的一种反模式。

让我们了解一下不该做什么。

模式 2 - 模拟/修补 requests 库

最直接的方法是模拟 requests 库。

这样可以确保测试不会调用真实的 API,从而避免上述提到的问题。

# tests/unit/test_pattern2.py
"""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})

在这段代码中,我们通过为 requests.post 使用模拟对象来测试 upload_file 函数,而无需实际发出 API 请求。

  1. 首先,我们定义了 stub_upload_response,这是一个预期的响应,模拟了调用 upload_file 时真实 API 会返回的内容。
  2. 我们在 src.file_uploader 中对 requests.post 使用 patch。这个临时补丁意味着在这个上下文内对 requests.post 的任何调用都会返回一个可控的模拟对象,而不是进行真正的 HTTP 请求。
  3. 我们创建了 mock_post_response,这是一个模拟对象,用于模拟 requests 的真实响应。我们设置了它的 status_code 和 json 方法,使其返回我们的 stub_upload_response
  4. 当 upload_file 运行时,它现在接收到的是我们的模拟响应,而不是真实响应。然后测试断言该函数的行为符合预期,检查成功状态和其他关键值。

我想问你,这种方法有什么问题,或者为什么它不可取呢?

假设你的老板要求你添加更多功能,比如 download_fileupdate_file 或 delete_file 功能。

想象一下为每个功能模拟 requests.post。再考虑一下设置和清理工作,你在下载或删除文件之前需要先上传文件。

在每种情况下进行模拟会变得非常复杂,与实际情况脱节,并且极难维护或调试。

让我们来回顾一下这种方法的优缺点。

  • 优点
  • 无需更改客户端代码:模拟允许你在不修改实际客户端代码的情况下测试 API 交互,这使得在被测试的函数中实现和维护测试变得简单直接。
  • 工作量小:模拟请求相对快速且容易,特别是对于简单的函数,这可以节省设置测试基础设施的时间和精力。
  • 许多开发者熟悉:大多数开发者对模拟库都很熟悉,因此这种方法无需陡峭的学习曲线,易于上手。
  • 缺点
  • 与实现紧密耦合:测试依赖于 requests 库的特定细节。对请求方式的任何更改(例如,从 requests.get 切换到 requests.Session().get)都可能导致测试失败,即使功能保持不变。
  • 脆弱且难以维护:随着需求的复杂性或规范的增加,模拟设置会变得越来越繁琐,难以管理,特别是如果许多测试依赖于类似的模拟。
  • 每个测试都需要额外的修补工作:你需要记住在每个可能触发 API 调用的测试中应用 @patch 或使用上下文管理器,这增加了测试设置的要求。
  • 可能混淆业务逻辑和 I/O 问题:这种方法很容易无意中将业务逻辑与 I/O 行为混合在一起,使测试的目的变得复杂,并导致测试设置混乱。
  • 可能需要集成和端到端测试:由于模拟不能保证代码与实际 API 一起工作,因此通常需要单独的集成和端到端测试来验证真实的 API 行为。

正如你所见,除了非常简单的代码之外,我不太喜欢这种方法。

那么解决方案是什么呢?

让我们看看其他想法,找到一个更可靠的方法。

模式 3 - 围绕第三方 API 构建适配器/包装器

我有一个有趣的想法,即适配器的概念:围绕第三方 API 或 I/O 的包装器。

这个包装器是为支持你与第三方 API 的交互而构建的,并且用你自己的语言编写(不是直接基于第三方的 Swagger 文档)。

它将底层的操作(如 requests.post 或 requests.get)从最终用户那里抽象出来,这样如果需要,就可以轻松地伪造、模拟甚至替换 API。

让我们看看如何编写这个包装器。

# 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()

在这个非常简单的适配器中,我们有一个 FileIOAdapter 类,其中包含 upload_file 方法。这个方法打开一个文件并将其内容上传到第三方 API。

我们如何测试它呢?

我们在这里也会使用模拟/补丁,但不是模拟 requests.post 库,而是对 FileIOAdapter 类的 upload_file 方法进行补丁操作。

# 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")

如果你观察上面的模拟操作,会发现它简单得多,并且使用的模拟对象更少。

我们只模拟了所需的方法,并使用了一个存根响应 {"success": True}

然后我们创建了 FileIOAdapter 类的一个实例,并在这个上下文中调用它的上传方法,这将返回 mock_upload_file 对象。

然后我们断言其交互情况。

与模拟与 request 库的底层交互相比,你可以看到这种方法容易得多。

  • 优点
  • 对模拟的控制:通过只模拟适配器,你避免了直接模拟像 requests 这样的外部依赖。这种方法降低了风险,简化了测试设置。
  • 将业务逻辑与外部 API 细节解耦:适配器抽象了底层的 API 调用,因此如果 API 发生变化或使用了新的提供商,你只需更新适配器,而无需更改业务逻辑。
  • 增强可维护性和信心:根据你自己的 API 术语而不是外部的具体细节进行测试,使测试更不容易出错,更具前瞻性,允许进行更安全的代码重构。
  • 缺点
  • 额外的代码和层次:引入适配器会增加一个额外的层次,增加代码的复杂性,特别是对于简单的集成。
  • 简单情况下可能产生不必要的开销:对于直接的 API 交互,这种模式可能显得过于复杂,在直接调用就足够的情况下添加了不必要的抽象。
模式 4 - 依赖注入

接下来,让我们看看另一种设计模式——依赖注入。

与其将业务逻辑(上传功能)硬编码到 FileUploader 类中,不如将其作为一个依赖项传递。

# src/file_uploader_adaptor_dep_injection.py
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

在这里,我们定义了一个抽象方法 FileUploader,其中包含一个 upload_file 方法,该方法需要由 FileIOUploader 类实现。

我们的业务逻辑 process_file_upload 很简单,只接受一个文件路径和一个上传器对象。

这对我们的测试有什么影响呢?让我们看看。

# tests/test_pattern4.py
"""Pattern 4: Dependency Injection"""

from src.file_uploader_dep_injection import process_file_upload, FileIOUploader

def test_process_file_upload():
    file_name = "sample.txt"
    file_io_uploader = FileIOUploader()
    response = process_file_upload(file_path=file_name, uploader=file_io_uploader)
    assert response["success"] is True
    assert response["name"] == file_name
    assert response["key"] is not None

代码相当简洁,但它仍然使用了真实的 API。我们该怎么做呢?

我们现在可以轻松地将一个模拟对象作为依赖项传递。

# tests/unit/test_pattern4.py
from unittest import mock

def test_process_file_upload_mock():
    mock_uploader = mock.Mock()
    mock_uploader.upload_file.return_value = {
        "success": True,
        "link": "https://file.io/abc123",
    }

    result = process_file_upload("test.txt", uploader=mock_uploader)
    assert result["success"] is True
    assert result["link"] == "https://file.io/abc123"
    mock_uploader.upload_file.assert_called_once_with("test.txt")

我们创建了一个模拟对象,并指定它有一个 upload_file 方法,该方法返回一个存根响应。

然后我们将这个模拟对象作为依赖项传递给 process_file_upload 函数。

我们“注入”依赖项的事实使我们能够在测试或运行时决定是使用真实的 FileUploader API 还是模拟对象,这使得我们的测试非常灵活。

  • 优点
  • 灵活的测试设置:依赖注入允许你轻松地替换模拟对象,从而可以在不依赖真实 API 的情况下轻松测试不同的场景。
  • 将业务逻辑与依赖项解耦process_file_upload 中的业务逻辑不依赖于 FileIOUploader 的具体实现细节,这使得代码更具模块化,更能适应变化。
  • 鼓励代码复用和简洁性:这种方法通过分离逻辑和依赖项,促进了组件的复用,并使代码更简洁、更易于测试。
  • 缺点
  • 额外的复杂性:引入抽象类和接口会增加层次,对于较简单的用例来说可能显得过于复杂。
  • 简单集成时设置工作量大:对于直接的功能,依赖注入可能显得过于繁琐,特别是当不需要替换依赖项时。
  • 小项目中可能产生不必要的开销:这种模式可能会给不需要高度测试灵活性或可互换依赖项的小项目增加额外的负担。
模式 5 - 使用依赖注入和伪对象

上一个模式(依赖注入)非常好,但仍然使用了模拟对象。

如果你想完全避免使用模拟对象呢?

你可以使用伪对象(Fake)代替。

伪对象(Fake)是一种不同于模拟(Mock)和补丁(Patch)的测试替身,简单来说,它是你试图替换的对象在内存中的表示形式。

模拟对象是简单的对象,能支持任何方法,并且你可以对其交互进行断言。而伪对象则构建了一个简化或功能化的真实对象替代品,它可以是一个类、数据库、外部API或其他任何东西。

让我们看看如何实现伪对象。

# 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:
        # 通过将文件路径“保存”到字典中来模拟上传
        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

这里我们有一个FakeFileIOUploader类,在初始化时创建一个内存中的已上传文件字典。upload_file方法会向这个字典中添加数据。

利用依赖注入设计,我们可以轻松地将这个伪对象注入到测试中。伪对象可以使用列表、字典,甚至是SQLite内存数据库来进行快速操作。

  • 优点
  • 测试更易读且更贴近现实:伪对象模拟真实对象的行为,使测试比使用模拟对象更直观,更接近现实场景。
  • 促进更好的设计:构建伪对象促使你仔细思考类和依赖项的结构,通常会带来更简洁、模块化的代码。
  • 无需依赖外部API:由于伪对象在内存中操作(例如使用字典或列表),你无需进行真实的API调用,从而提高了测试的可靠性和速度。
  • 缺点
  • 测试代码量增加:创建一个功能完善的伪对象需要更多的初始设置和维护工作,尤其是对于复杂的API,这可能导致测试代码量更大、更复杂。
  • 同步复杂性:伪对象必须与真实API的行为保持同步,因此API的任何变化都可能需要更新伪对象,增加了维护成本。
  • 小型测试可能存在不必要的开销:对于简单的API交互,功能完备的伪对象可能显得过于复杂,在使用模拟对象就足够的情况下引入了不必要的复杂性 。

通过依赖注入,我们可以轻松地使用不同的选项(真实API、模拟对象甚至伪对象)来运行代码,测试我们的系统,而无需更改process_upload_file方法。是不是很巧妙?

模式6 - 使用沙盒API

使用沙盒API是测试外部API交互的一种实际可行的方法。

许多第三方提供商,如Stripe,都提供模拟生产环境的沙盒环境,允许你在不影响真实数据或产生费用的情况下,测试实际的API行为。

与使用实时API相比,这种设置提供了更可靠的集成测试,因为它是专门为测试而设计的。

然而,虽然沙盒测试比直接使用生产环境更安全,但通常比使用伪对象或模拟对象要慢,并且你必须记得清理数据,以避免沙盒中存在大量杂乱的数据。

  • 优点
  • 真实的测试环境:沙盒API与生产环境非常相似,允许你在不影响实时数据的情况下,使用真实的API结构、错误处理和响应时间进行测试。
  • 维护负担较小:由于你无需创建测试替身或模拟对象,因此设置工作较少,并且在API发生变化时,也无需更新测试对象。
  • 准确的数据验证:沙盒环境支持在安全的环境中进行数据验证和响应处理的端到端测试,提高了对集成准确性的信心。
  • 缺点
  • 测试执行速度较慢:沙盒环境涉及真实的网络调用,这本质上比使用伪对象或模拟对象的测试要慢。
  • 访问或功能受限:一些提供商限制沙盒的功能或使用速率,这可能会限制测试,特别是对于边缘情况的测试。
  • 可靠性问题:尽管沙盒环境比实时API更稳定,但仍可能出现停机或偶尔的不一致情况,尤其是在未与真实API保持同步更新的情况下。
额外模式7 - 使用responses库

虽然上述6种模式为你提供了丰富的选择,但我还想介绍一些额外的测试模式。

Sentry的responses库是一个围绕requests的简洁模拟包装器,可让你轻松进行测试。

实现方式如下:

# tests/unit/test_pattern7.py
"""Pattern 7: Mocking external API calls using responses library"""

import responses
from src.file_uploader import upload_file


@responses.activate
def test_upload_file_success():
    # 模拟file.io的成功响应
    responses.add(
        responses.POST,
        "https://file.io",
        json={"success": True, "link": "https://file.io/abc123"},
        status=200,
    )

    result = upload_file("sample.txt")
    assert result == {"success": True, "link": "https://file.io/abc123"}
    assert responses.calls[0].response.status_code == 200

使用这个库,你可以测试各种交互,相当实用。

额外模式8 - 使用VCR.py

我最近发现了一个非常有趣的概念(可能早已存在),即使用VCR.py。

它会将HTTP交互记录在一个盒式磁带文件(YAML格式)中,并在未来的测试运行中重放这些记录。

这使你能够在不重复进行真实API调用的情况下,测试函数的行为。

# tests/unit/test_pattern8.py
import vcr
from src.file_uploader import upload_file


@vcr.use_cassette("tests/cassettes/upload_file.yaml")
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

通过这种设置,测试将仅在第一次运行时使用真实的API调用,之后将依赖记录的响应,从而使测试更快、更可靠。

  • 第一次运行:1.23秒
  • 第二次运行:0.25秒

@vcr.use_cassette装饰器将VCR.py应用于测试函数,并指定upload_file.yaml作为盒式磁带文件。

测试第一次运行时,VCR.py会将HTTP请求和响应保存到这个文件中。

在后续运行中,它将重放保存的响应,而无需进行真实的API调用。

  • 优点
  • 无需网络依赖的真实测试:VCR.py捕获真实的API响应,提供真实的数据,同时避免了对重复网络调用的需求。
  • 提高测试速度和可靠性:一旦请求记录在盒式磁带文件中,后续测试会更快,并且不太容易出现不稳定的情况,因为它们不依赖外部API的可用性。
  • 降低API成本和速率限制问题:VCR.py最大限度地减少了对实时API调用的需求,这可以降低成本并避免达到速率限制。
  • 易于共享的测试数据:盒式磁带文件以可读的YAML格式存储API响应,便于检查或与团队成员共享数据,以确保一致性。
  • 缺点
  • API变化时盒式磁带文件可能过时:如果API的行为或响应格式发生变化,现有的盒式磁带文件可能会过时。
  • 初始设置复杂:配置和管理盒式磁带文件会增加复杂性,特别是对于有许多端点的大型项目,盒式磁带文件的组织可能会变得具有挑战性。
  • 动态场景覆盖有限:对于高度动态的数据,VCR.py可能无法捕获所有可能的响应变化,这可能导致测试覆盖不完整,或者需要频繁更新盒式磁带文件。
  • 管理盒式磁带文件可能产生额外负担:积累大量盒式磁带文件会增加管理负担,特别是在更新频繁的情况下。

尽管如此,我仍然觉得这个概念很有趣。

额外模式9 - 契约测试

契约测试是一种测试策略,用于验证服务或组件之间的交互是否符合预定义的“契约”。

通过确保各方都遵守商定的结构(即契约),可以减少集成问题,并提高对复杂分布式系统的信心。

在我们的upload_file函数中,契约将定义向https://file.io发送包含文件数据的POST请求,并且API应返回一个包含successlinkkeyname键的JSON对象。

然后,我们可以使用像Pact这样的契约测试工具,它允许你定义契约,验证代码生成的请求是否符合契约,并根据契约验证响应。

本文不涉及契约测试的详细内容,如果你感兴趣,请告诉我,我会详细介绍。

  • 优点
  • 确保请求和响应的一致性:验证upload_file函数符合预期的API结构。
  • 及早发现API变化:如果API提供商更改了API,这个测试将捕获到变化,防止运行时错误。
  • 起到文档作用:作为预期API交互的文档,对团队和API提供商都很有用。
  • 减少测试的不稳定性:减少对脆弱模拟对象的依赖,特别是在API发生变化时。
  • 缺点
  • 设置复杂或有额外开销:需要安装和设置契约测试工具。
  • 前期投入较大:比简单的模拟设置更复杂,但能提供更长期的稳定性。
  • 需要与提供商协调:为了充分发挥作用,API提供商应验证契约,这可能需要额外的协调工作。
额外模式10 - 使用WireMock

我在希蒙·米克斯(Szymon Miks)的博客中看到了另一个有趣的概念——WireMock。

很多人在Reddit上推荐它,所以我想尝试一下。

从其官网介绍可知:WireMock让你摆脱对不稳定API的依赖,使你能够自信地进行开发。启动一个模拟API服务器并模拟各种现实场景和API(包括REST、SOAP、OAuth2等)非常容易。

让我们看看如何使用它。他们还有一个名为python - wiremock的Python库,为WireMock提供了Python接口。

要使用它,你需要安装Docker并使其运行。

你还需要两个依赖项——wiremock和testcontainers,它们包含在仓库的requirements.txt文件中。

在编写测试之前,我们对应用程序代码做一个小改动。我们将API URL作为参数传递,而不是硬编码。

这使我们能够轻松使用WireMock,而无需模拟或修补API URL,这样我们的应用程序就会使用WireMock的URL,而不是真实的URL。

# src/file_uploader.py
import requests


def upload_file_param(file_name, base_url):
    with open(file_name, "rb") as file:
        response = requests.post(base_url, files={"file": file})
        response.raise_for_status()
        upload_data = response.json()

        if response.status_code == 200:
            print(f"File uploaded successfully. Upload data: {upload_data}")
            return upload_data
        else:
            raise Exception("File upload failed.")

现在编写测试:

# tests/unit/test_pattern10.py
from src.file_uploader import upload_file_param
import pytest
from wiremock.testing.testcontainer import wiremock_container
from wiremock.constants import Config
from wiremock.client import *


@pytest.fixture
def wiremock_server():
    # 使用映射设置WireMock服务器
    with wiremock_container(secure=False) as wm:
        # 设置WireMock管理端的基础URL(仅用于WireMock的设置)
        Config.base_url = wm.get_url("__admin")

        # 映射上传端点以获得成功响应
        Mappings.create_mapping(
            Mapping(
                request=MappingRequest(method=HttpMethods.POST, url="/"),
                response=MappingResponse(
                    status=200,
                    json_body={
                        "success": True,
                        "link": "https://file.io/TEST",
                        "key": "TEST",
                        "name": "sample.txt",
                    },
                ),
                persistent=False,
            )
        )

        yield wm  # 将WireMock实例提供给测试


def test_upload_file_success(wiremock_server):
    response = upload_file_param("sample.txt", wiremock_server.get_url("/"))

    assert response["success"] is True
    assert response["link"] == "https://file.io/TEST"
    assert response["name"] == "sample.txt"
    assert response["key"] == "TEST"

wiremock_server夹具启动一个WireMock容器作为API的模拟服务器。 它配置Config.base_url指向WireMock的管理端点,以便进行端点映射设置。 创建了一个对/URL的POST请求的映射,模拟成功响应。 这个夹具生成正在运行的WireMock服务器,供测试使用。 测试函数test_upload_file_success使用wiremock_server,通过模拟服务器的URL调用upload_file_param。 我们像正常调用一样运行测试,唯一的区别是注入了WireMock服务器的URL,而不是真实的URL。

  • 优点
  • 真实的API模拟:WireMock能紧密模拟实际的API响应,允许精确控制各种场景,如错误或超时。
  • 一致性:它在测试运行之间提供稳定的响应,避免测试不稳定,并且不依赖实时API的可用性。
  • 复杂场景处理能力:支持条件响应、延迟和故障模拟,有助于测试边缘情况和错误处理。
  • 自动化和隔离测试:可以轻松集成到CI管道中,进行隔离的自动化测试,而不会影响实时API。
  • 缺点
  • 设置复杂:需要为每个端点和响应场景进行配置,维护起来可能很耗时。
  • 场景固定:如果真实API发生变化,模拟可能会过时,并且无法捕捉任何意外行为。
  • 资源密集:运行WireMock服务器,特别是在CI环境中,比轻量级的模拟选项消耗更多资源。
  • 并非完全端到端测试:虽然它模拟了API,但WireMock不涵盖实际的实时API集成,这部分可能仍然需要测试。
结论

在这篇篇幅较长但很重要的文章中,你学习了许多测试第三方集成的方法。

你探索了多种技术,从真实测试开始,接着是模拟、伪对象、依赖注入、适配器和沙盒测试。

此外,你还学习了使用responses库、VCR.py和WireMock在本地模拟外部API。

除了单纯模拟requests库之外,所有这些技术都很可靠,为测试外部API集成提供了坚实的基础。

本文最重要的概念是权衡(优缺点),因为你做出的每一个决策都有其优点和缺点。

选择你需要的优点且能接受其缺点的方法。