乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • Spring AI 学习路径
  • 第1章 Spring AI 概述与核心理念

    • 章节导读
    • Spring AI 概述与可移植 API
    • 核心模块与依赖关系
  • 第2章 快速入门与第一个AI应用

    • 章节导读
    • 环境准备与配置
    • 第一个 AI 对话应用
  • 第3章 聊天模型与ChatClient

    • 章节导读
    • ChatClient 详解
    • ChatModel 底层抽象
    • 多轮对话与 ChatMemory
  • 第4章 提示词管理与模板

    • 章节导读
    • Prompt 与 Message 体系
    • 提示词模板与动态构建
  • 第5章 输出解析与结构化响应

    • 章节导读
    • 输出解析器与 BeanOutputConverter
  • 第6章 嵌入模型与向量存储

    • 章节导读
    • EmbeddingModel 与文本向量化
    • ETL 数据注入流水线
    • 向量存储抽象与配置
  • 第7章 检索增强生成(RAG)

    • 章节导读
    • RAG 核心机制与流程
    • QuestionAnswerAdvisor 详解
    • RAG 实战案例
  • 第8章 函数调用(Function Calling)

    • 章节导读
    • Tool 注解与函数声明
    • 函数调用实战
  • 第9章 多模态

    • 章节导读
    • 图像输入与视觉模型
    • 图像生成
  • 第10章 Advisor拦截器链

    • 章节导读
    • Advisor 链与内置拦截器
  • 第11章 MCP 协议与跨语言工具集成

    • 章节导读
    • MCP 协议深入
  • 第12章 可观测性测试与最佳实践

    • 章节导读
    • 可观测性与指标监控
    • 测试策略与 Mock 实践
    • 最佳实践与面试考点汇总

小崔推了推眼镜:"测试 AI 应用?每次都调 OpenAI API 那测试成本得上天。"白歌递来笔记本:"Mock ChatModel 和 Testcontainers——一个单元测试,一个集成测试,完美覆盖。"

测试策略与 Mock 实践

定义与作用

AI 应用的测试面临特殊挑战:API 调用有成本且不稳定,向量存储依赖外部中间件。Spring AI 的测试策略分为两层:单元测试用 @MockBean 隔离 ChatModel,集成测试用 Testcontainers 提供真实 VectorStore 环境。

核心原理

图释:单元测试关注 ChatClient 的 Advisor 链和工具调度逻辑,用 Mock 隔离 LLM;集成测试关注 ETL Pipeline 和 RAG 检索链路,用 Testcontainers 提供真实的向量数据库。

完整示例一:单元测试——Mock ChatModel

场景说明

小崔需要测试一个带工具调度的 ChatClient,验证工具是否被正确调用,但不希望真的调用 OpenAI API。

@SpringBootTest
@AutoConfigureMockMvc
public class ChatClientUnitTest {

    @MockBean
    private ChatModel chatModel;  // Mock 底层模型,避免真实 API 调用

    @Autowired
    private ChatClient.Builder chatClientBuilder;

    private ChatClient chatClient;

    @BeforeEach
    void setUp() {
        chatClient = chatClientBuilder
            .defaultTools(new WeatherTools())  // 真实工具对象
            .build();
    }

    @Test
    void testToolCallingFlow() {
        // 1. 预设 AI 返回"工具调用"指令
        AssistantMessage toolCallMessage = new AssistantMessage(
            "获取天气信息",
            Map.of(),
            List.of(new ToolCall(
                "getWeather",
                "getWeather",
                new HashMap<>(Map.of("city", "广州"))
            ))
        );

        ChatResponse mockResponse = new ChatResponse(
            List.of(new Generation(toolCallMessage))
        );

        when(chatModel.call(any(Prompt.class)))
            .thenReturn(mockResponse);

        // 2. 执行
        String result = chatClient.prompt()
            .user("今天广州天气怎么样?")
            .call()
            .content();

        // 3. 验证
        assertThat(result).contains("广州");
        verify(chatModel, atLeastOnce()).call(any(Prompt.class));
    }

    @Test
    void testAdvisorModifiesPrompt() {
        // 预设 AI 简单回复
        AssistantMessage aiReply = new AssistantMessage(
            "你好!我是飞翔科技智能助手。有什么可以帮助你?");
        ChatResponse mockResponse = new ChatResponse(
            List.of(new Generation(aiReply))
        );

        when(chatModel.call(any(Prompt.class)))
            .thenAnswer(invocation -> {
                Prompt actualPrompt = invocation.getArgument(0);
                String userContent = actualPrompt.getInstructions().stream()
                    .filter(m -> m.getMessageType() == MessageType.USER)
                    .map(Message::getContent)
                    .findFirst()
                    .orElse("");
                // 验证 Prompt 中是否被 Advisor 注入了内容
                assertThat(userContent).doesNotContain("SYSTEM_INJECT");
                return mockResponse;
            });

        String result = chatClient.prompt()
            .user("测试消息")
            .call()
            .content();

        assertThat(result).contains("飞翔科技");
    }
}

运行结果

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

测试用时 < 1 秒(全部 Mock,零网络开销),相比真实 API 调用节约了时间和费用。

完整示例二:集成测试——Testcontainers + PGVector

场景说明

白歌要求验证 ETL 注入和 RAG 检索链路,确保 VectorStore 配置正确、SimilaritySearch 返回符合预期的文档片段。

依赖引入

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
public class RagIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
        "pgvector/pgvector:pg16"
    )
        .withDatabaseName("vectordb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private VectorStore vectorStore;

    @Test
    void testDocumentIngestionAndSearch() {
        // 1. 注入测试文档
        Document doc1 = new Document(
            "数据结构是计算机科学的基础课程,涵盖线性表、树、图等核心概念。",
            Map.of("course", "CS101", "teacher", "白歌")
        );
        Document doc2 = new Document(
            "线性代数主要研究向量空间和线性变换,是机器学习的基础数学工具。",
            Map.of("course", "MATH201", "teacher", "大翔")
        );

        vectorStore.add(List.of(doc1, doc2));

        // 2. 语义检索
        SearchRequest request = SearchRequest.query("计算机基础课程有哪些")
            .withTopK(2)
            .withSimilarityThreshold(0.6);

        List<Document> results = vectorStore
            .similaritySearch(request);

        // 3. 验证
        assertThat(results).isNotEmpty();
        assertThat(results.get(0).getContent())
            .contains("数据结构");
        assertThat(results.get(0).getMetadata())
            .containsEntry("course", "CS101");
    }

    @Test
    void testMetadataFiltering() {
        Document doc1 = new Document(
            "机器学习实战课程", Map.of("level", "advanced")
        );
        Document doc2 = new Document(
            "编程基础入门", Map.of("level", "beginner")
        );
        vectorStore.add(List.of(doc1, doc2));

        SearchRequest request = SearchRequest
            .query("适合新手的课程")
            .withFilterExpression("level == 'beginner'")
            .withTopK(1);

        List<Document> results = vectorStore
            .similaritySearch(request);

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getContent())
            .contains("基础入门");
    }
}

运行结果

Container is started (pgvector:pg16)
[VectorStore] Added 2 documents
[SimilaritySearch] Query: "计算机基础课程有哪些"
[SimilaritySearch] Results: 2 matches
  #1 [score=0.92] 数据结构是计算机科学的基础课程...
  #2 [score=0.78] 线性代数主要研究向量空间...

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

测试策略总结

测试层次工具Mock 对象验证目标
单元测试@MockBeanChatModelAdvisor 逻辑、工具调度
集成测试Testcontainers无(真实数据库)ETL + 检索链路
端到端测试WireMock外部 AI API完整请求-响应流程

易错场景与面试考点

易错场景一:忘记 @MockBean 导致真实 API 调用

// ❌ 错误:没有 Mock ChatModel
@SpringBootTest
public class ChatTest {
    @Autowired
    private ChatClient chatClient;

    @Test
    void testSomething() {
        // 真的调用了 OpenAI API!
        chatClient.prompt().user("hello").call().content();
    }
}

问题分析

测试环境没有真实的 OpenAI API Key,测试会失败。即使有,也会产生费用。

// ✅ 正确:Mock ChatModel
@SpringBootTest
public class ChatTest {
    @MockBean
    private ChatModel chatModel;

    @Autowired
    private ChatClient.Builder builder;

    @Test
    void testSomething() {
        when(chatModel.call(any(Prompt.class)))
            .thenReturn(new ChatResponse(
                List.of(new Generation(
                    new AssistantMessage("Mock reply")))));
        // 零费用、确定性测试
    }
}

面试高频题

Q1:Spring AI 应用的测试策略是什么?

分层测试:单元测试 Mock ChatModel,验证 Advisor 链和工具调度;集成测试用 Testcontainers 真实 VectorStore 验证 RAG 链路;端到端测试用 WireMock 模拟外部 API 验证完整流程。核心原则是"隔离外部依赖、保真内部逻辑"。

Q2:为什么测试 RAG 要用真实 VectorStore?

VectorStore 的相似性搜索涉及 Embedding 模型调用和向量距离计算,Mock 无法验证检索结果的准确性和相关性排序。Testcontainers 提供真实数据库环境,确保 ETL 注入和检索链路在生产部署前得到充分验证。

本章小结

  • @MockBean 隔离 ChatModel,零成本快速验证业务逻辑
  • Testcontainers 提供真实 VectorStore 环境,保障 RAG 链路可靠性
  • 分层测试策略:单元 → 集成 → 端到端
  • Mock 应覆盖:Advisor 逻辑、工具调度、异常处理
上一页
可观测性与指标监控
下一页
最佳实践与面试考点汇总