You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对需要调用外部服务API的Python类进行单元测试?

如何对需要调用外部服务API的Python类进行单元测试?

这个类用于封装一个远程托管的大语言模型,需要调用服务API来获取结果,示例代码如下:

from typing import Iterator, List
from anthropic import AnthropicVertex
from your_abstract_class import AbstractLLMInterface
from your_types import Correspondence
import llm_logger
from your_config import CONFIGS, get_gcp_project_id

class ModelWrapper(AbstractLLMInterface):
    """The Claude 3 Sonnet model wrapper following the interface."""

    def __init__(
        self,
        region: str = CONFIGS["GCP"]["REGION"],
        project: str = get_gcp_project_id(),
        model: str = CONFIGS["MODELS"]["SONNET_ID"],
    ) -> None:
        """Set up the claude client using the region and project."""
        self.client: AnthropicVertex = AnthropicVertex(region=region, project_id=project)
        self.model_name: str = model
        self.role_key: str = "role"
        self.content_key: str = "content"
        self.user_key: str = "user"
        llm_logger.debug(msg=f"Initialised sonnet client for {region}, {project} and {self.model_name}.")

    def get_completion(self, user_prompt: str, system_prompt: str, history: List[Correspondence]) -> Iterator[str]:
        """
        Fetch a response from the model.This requires an egress request to GCP and the
        service for Anthropic model must be enabled in the VertexAI console.
        """

        # This is where the API call to GCP service happens
        return self.client.messages.stream(user_prompt, system_prompt, history)

我知道Python单元测试里的MagicMocks,可以配置成返回任意内容。但在这个例子里,构造函数的参数都是简单字符串,而且client是在类内部创建的,看起来好像没有地方可以注入模拟客户端,对吗?那是不是意味着这类单元测试必须发起真实的API调用?还是说这个类的设计本身有问题?希望能得到帮助,这个类应该可以在不调用API的情况下被测试。

注意:我知道如果修改类的构造函数,让它接受一个AnthropicVertex对象的话,模拟就会变得非常简单。但我的问题是要测试当前这个已有的类,而不是修改它。不过,如果当前类的设计违反了任何基础设计原则(比如SOLID),我也很想了解清楚,相关的参考说明也很欢迎。


先说说设计上的问题

首先得明确:这个类的设计确实违反了SOLID中的依赖倒置原则——它作为高层的业务封装模块(ModelWrapper),直接依赖了具体的底层实现(AnthropicVertex客户端),而不是依赖抽象接口。这种紧耦合的设计直接导致了测试困难,因为我们没办法轻松替换掉真实的客户端实例。

不修改现有类的前提下,怎么实现无API调用测试?

如果不能改动现有类的代码,我们可以用**猴子补丁(Monkey-Patching)**的方式,借助Python标准库unittest.mock里的patch工具,替换掉真实的客户端类,从而规避真实的API请求。

给你举个具体的测试代码示例:

from unittest.mock import patch, MagicMock
from typing import List
from your_module import ModelWrapper, Correspondence

def test_model_wrapper_completion():
    # 预设模拟的流式返回结果
    mock_stream_response = iter(["这是模拟的模型返回内容"])

    # 用patch替换掉ModelWrapper里用到的AnthropicVertex类
    with patch("your_module.AnthropicVertex") as mock_anthropic_cls:
        # 创建模拟的客户端实例,配置它的stream方法返回预设内容
        mock_client = MagicMock()
        mock_client.messages.stream.return_value = mock_stream_response
        # 让模拟的AnthropicVertex类返回这个模拟实例
        mock_anthropic_cls.return_value = mock_client

        # 初始化ModelWrapper,此时它的client属性是我们的模拟对象
        wrapper = ModelWrapper(region="test-region", project="test-project", model="test-model")

        # 调用要测试的get_completion方法
        result = list(wrapper.get_completion(
            user_prompt="你好",
            system_prompt="你是一个助手",
            history=[]
        ))

        # 断言结果符合预期
        assert result == ["这是模拟的模型返回内容"]
        # 验证API方法是否被正确调用(参数是否符合预期)
        mock_client.messages.stream.assert_called_once_with(
            "你好", "你是一个助手", []
        )

这个方法的核心逻辑是:patch("your_module.AnthropicVertex")会在测试运行的上下文里,把当前模块中引用的AnthropicVertex替换成一个MagicMock类。当ModelWrapper的构造函数创建AnthropicVertex实例时,实际上得到的是我们预先配置好的模拟客户端,后续调用messages.stream时就会返回我们预设的模拟数据,完全不会触发真实的API请求。

补充:如果后续可以重构代码,怎么优化设计?

如果之后有机会重构这个类,建议遵循依赖倒置原则,让ModelWrapper依赖一个抽象的客户端接口,而不是具体的AnthropicVertex。比如先定义一个抽象基类:

from abc import ABC, abstractmethod
from typing import Iterator, List
from your_types import Correspondence

class AbstractLLMClient(ABC):
    @abstractmethod
    def stream_completion(self, user_prompt: str, system_prompt: str, history: List[Correspondence]) -> Iterator[str]:
        pass

然后让AnthropicVertex的适配类实现这个接口,再把ModelWrapper的构造函数改成接受这个抽象接口的实例:

class ModelWrapper(AbstractLLMInterface):
    def __init__(self, client: AbstractLLMClient, model_name: str):
        self.client = client
        self.model_name = model_name
        # 其他初始化逻辑...

    def get_completion(self, user_prompt: str, system_prompt: str, history: List[Correspondence]) -> Iterator[str]:
        return self.client.stream_completion(user_prompt, system_prompt, history)

这样测试的时候,只需要创建一个实现了AbstractLLMClient的模拟类(或者直接用MagicMock),传入ModelWrapper即可,完全不需要猴子补丁,代码的扩展性也会好很多——后续要替换成其他LLM的客户端,只需要实现同一个抽象接口就行。

备注:内容来源于stack exchange,提问作者dell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 14:05:29