在开发依赖 Anthropic Claude 系列模型(如claude-mythos-5、claude-fable-5、claude-opus-5)的应用程序时,单元测试的编写往往面临两大挑战:一是需要真实的 API 密钥和网络连接,二是真实调用会产生费用和延迟。为了解决这些问题,我们可以设计一个 Mock 测试类,它完全模拟官方 Python SDK 的调用接口,使得测试代码与生产代码保持一致,同时无需任何外部依赖。
本文详细介绍了MockAnthropic类的设计与应用,它提供了一个与官方 Anthropic Python SDK 完全兼容的模拟客户端,支持多模态输入(图片、文档)和所有常见调用模式。通过使用该 Mock 类,开发者可以在单元测试中高效地验证代码逻辑,而无需关心网络、密钥或成本问题。未来可根据需要进一步扩展流式响应、异步调用等功能,以满足更复杂的测试需求。
MockAnthropic 类的设计与实现
核心思想是模拟anthropic.Anthropic客户端的行为,特别是client.messages.create方法。我们使用SimpleNamespace构建与官方 SDK 返回结构兼容的对象,使得测试代码可以直接通过message.content[0].text获取文本,而无需任何改动。
完整代码
fromtypesimportSimpleNamespaceclassMockMessages:"""模拟 `client.messages` 对象,支持文本、图片和文档附件。"""SUPPORTED_MODELS={"claude-mythos-5","claude-fable-5","claude-opus-5"}defcreate(self,model,messages,max_tokens=1024,system=None,**kwargs):""" 模拟 `client.messages.create` 调用,支持文本、图片和文档输入。 参数与官方 SDK 一致: - model: 模型名称 - messages: 消息列表,content 可以是字符串或块列表(文本、图片、文档等) - max_tokens: 最大生成 token 数 - system: 系统提示词(可选) - **kwargs: 其他参数(忽略) 返回模拟的 Message 对象,结构与官方 SDK 返回一致。 """ifmodelnotinself.SUPPORTED_MODELS:raiseValueError(f"Unsupported model:{model}. Supported models:{self.SUPPORTED_MODELS}")# 提取最后一条用户消息的内容,支持字符串和块列表user_text=""media_info=[]# 存储图片和文档信息formsginreversed(messages):ifmsg.get("role")=="user":content=msg.get("content","")ifisinstance(content,str):user_text=contentelifisinstance(content,list):text_parts=[]forblockincontent:block_type=block.get("type")ifblock_type=="text":text_parts.append(block.get("text",""))elifblock_typein("image","document"):source=block.get("source",{})source_type=source.get("type","unknown")media_type=source.get("media_type","unknown")ifblock_type=="image":ifsource_type=="base64":data=source.get("data","")media_info.append(f"[Image:{media_type}, base64 data length:{len(data)}]")elifsource_type=="url":url=source.get("url","")media_info.append(f"[Image URL:{url}, type:{media_type}]")else:media_info.append(f"[Image:{media_type}, source type:{source_type}]")elifblock_type=="document":filename=block.get("filename","unnamed")ifsource_type=="base64":data=source.get("data","")media_info.append(f"[Document:{filename},{media_type}, base64 data length:{len(data)}]")elifsource_type=="url":url=source.get("url","")media_info.append(f"[Document URL:{filename},{url}, type:{media_type}]")else:media_info.append(f"[Document:{filename},{media_type}, source type:{source_type}]")user_text=" ".join(text_parts)break# 构造模拟回复文本system_prefix=f"[System:{system}] "ifsystemelse""media_prefix=" ".join(media_info)+" "ifmedia_infoelse""response_text=f"{system_prefix}{media_prefix}Mock response from{model}. You said:{user_text}"# 根据 max_tokens 粗略截断(每 token 按 4 字符估算)ifmax_tokensisnotNoneandmax_tokens>0:response_text=response_text[:max_tokens*4]# 构建模拟的 Message 对象(与官方 SDK 返回结构兼容)mock_content_block=SimpleNamespace(type="text",text=response_text)mock_usage=SimpleNamespace(input_tokens=len(user_text.split())+(len(system.split())ifsystemelse0),output_tokens=len(response_text.split()))mock_message=SimpleNamespace(id="msg_mock_12345",type="message",role="assistant",content=[mock_content_block],model=model,stop_reason="end_turn",stop_sequence=None,usage=mock_usage)returnmock_messageclassMockAnthropic:""" 模拟 Anthropic 官方客户端,用法与 `anthropic.Anthropic` 完全一致,支持多模态与文档附件。 """def__init__(self,api_key=None,**kwargs):# 可接受 api_key 等参数,但全部忽略self.messages=MockMessages()关键设计说明
- 接口一致性:
MockAnthropic类暴露messages属性,其类型为MockMessages,提供create方法,与官方client.messages.create签名一致。 - 消息解析:
create方法遍历消息列表,提取最后一条用户消息的内容。内容可以是字符串或列表,列表中的块类型支持text、image和document。 - 多模态识别:对于图片和文档块,提取其来源信息(base64 数据长度或 URL)并生成描述性文本,使模拟回复能够反映输入内容。
- 响应构造:使用
SimpleNamespace构造包含content、usage等属性的对象,content是一个列表,包含一个type="text"的块,与真实响应结构一致。 - 错误处理:如果传入不支持的模型名称,会抛出
ValueError,模拟真实 SDK 的行为。 - 可扩展性:若需要支持流式响应或异步调用,可在此基础上添加相应方法。
所有类型的调用语句示例
以下使用MockAnthropic客户端演示各种可能的调用场景,并展示模拟输出结果。
1. 非多模态:纯文本字符串
client=MockAnthropic()message=client.messages.create(model="claude-opus-5",max_tokens=1024,messages=[{"role":"user","content":"Hello, Claude!"}])print(message.content[0].text)输出:
Mock response from claude-opus-5. You said: Hello, Claude!2. 非多模态:文本块列表
message=client.messages.create(model="claude-fable-5",max_tokens=512,messages=[{"role":"user","content":[{"type":"text","text":"Tell me a joke."}]}])print(message.content[0].text)输出:
Mock response from claude-fable-5. You said: Tell me a joke.3. 非多模态:带系统提示词
message=client.messages.create(model="claude-mythos-5",max_tokens=1024,system="You are a helpful assistant.",messages=[{"role":"user","content":"What is AI?"}])print(message.content[0].text)输出:
[System: You are a helpful assistant.] Mock response from claude-mythos-5. You said: What is AI?4. 多模态:文本 + Base64 图片
base64_image="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="message=client.messages.create(model="claude-opus-5",max_tokens=1024,messages=[{"role":"user","content":[{"type":"text","text":"What's in this image?"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":base64_image}}]}])print(message.content[0].text)输出:
[Image: image/png, base64 data length: 68] Mock response from claude-opus-5. You said: What's in this image?5. 多模态:文本 + 图片 URL
message=client.messages.create(model="claude-fable-5",max_tokens=512,messages=[{"role":"user","content":[{"type":"text","text":"Describe this picture."},{"type":"image","source":{"type":"url","url":"https://example.com/image.jpg"}}]}])print(message.content[0].text)输出:
[Image URL: https://example.com/image.jpg, type: unknown] Mock response from claude-fable-5. You said: Describe this picture.6. 多模态:文本 + Base64 文档(PDF)
base64_pdf="JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nGNgYGJgYGAQZBBhYBSLAGQzB8hgsDAwgATRDAxMgUwGKMDAwMDAwsDAxMDAwMDCwMDAwMDwP0gAISYGBgYGBgYG"message=client.messages.create(model="claude-opus-5",max_tokens=2048,messages=[{"role":"user","content":[{"type":"text","text":"Summarize this document."},{"type":"document","source":{"type":"base64","media_type":"application/pdf","data":base64_pdf},"filename":"report.pdf"}]}])print(message.content[0].text)输出:
[Document: report.pdf, application/pdf, base64 data length: 274] Mock response from claude-opus-5. You said: Summarize this document.7. 多模态:文本 + 文档 URL
message=client.messages.create(model="claude-fable-5",max_tokens=1024,messages=[{"role":"user","content":[{"type":"text","text":"Extract key points from this file."},{"type":"document","source":{"type":"url","url":"https://example.com/document.txt"},"filename":"notes.txt"}]}])print(message.content[0].text)输出:
[Document URL: notes.txt, https://example.com/document.txt, type: unknown] Mock response from claude-fable-5. You said: Extract key points from this file.8. 多模态:混合文本、图片和文档
message=client.messages.create(model="claude-opus-5",max_tokens=4096,messages=[{"role":"user","content":[{"type":"text","text":"Analyze the image and the document together."},{"type":"image","source":{"type":"url","url":"https://example.com/chart.png"}},{"type":"document","source":{"type":"base64","media_type":"text/plain","data":"SGVsbG8gV29ybGQ="},"filename":"data.txt"}]}])print(message.content[0].text)输出:
[Image URL: https://example.com/chart.png, type: unknown] [Document: data.txt, text/plain, base64 data length: 16] Mock response from claude-opus-5. You said: Analyze the image and the document together.如何在测试中使用 MockAnthropic
直接实例化
最简单的使用方式是在测试代码中直接创建MockAnthropic实例,并将其传递给被测函数(通过依赖注入或参数传递)。
defask_claude(prompt,client):message=client.messages.create(model="claude-opus-5",max_tokens=1024,messages=[{"role":"user","content":prompt}])returnmessage.content[0].text# 测试代码deftest_ask_claude():mock_client=MockAnthropic()result=ask_claude("Hello",mock_client)assert"Mock response"inresult使用 unittest.mock.patch 替换
如果不希望修改生产代码的客户端创建逻辑,可以使用patch将anthropic.Anthropic替换为MockAnthropic。
fromunittest.mockimportpatchimportanthropicdefask_claude(prompt):client=anthropic.Anthropic()message=client.messages.create(model="claude-opus-5",max_tokens=1024,messages=[{"role":"user","content":prompt}])returnmessage.content[0].textdeftest_ask_claude_with_patch():withpatch('anthropic.Anthropic',MockAnthropic):result=ask_claude("Hello")assert"Mock response"inresult通过这种方式,测试代码与生产代码保持完全一致,只需在测试环境中替换客户端类即可。
优点与局限性
优点
- 零外部依赖:无需网络、API 密钥或真实服务,测试可离线运行。
- 接口完全兼容:调用语法与官方 SDK 相同,降低学习成本,且测试代码可直接迁移到生产。
- 支持多模态:能够处理图片和文档附件,覆盖更多真实场景。
- 灵活可定制:可以根据需要调整模拟回复的内容或行为(例如模拟错误、特定输出等)。
局限性
- 模拟响应不够真实:返回的文本是模板化的,无法验证业务逻辑对实际模型输出多样性的处理。
- 缺少高级功能:尚未实现流式响应、异步调用、工具调用(Tool Use)等高级特性(但可扩展)。
- token 估算粗略:模拟的 token 计数基于简单的字符/单词估算,可能与真实 SDK 不一致。