第五单元——第10课:用自动化测试验证工具逻辑

上一课验证了"重复请求不会重复保存"。但以后修改代码,怎样知道这个功能没有被改坏?

这节把人工观察变成自动检查:

  1. 相同请求只保存一条记录。
  2. 同一请求编号不能更换内容。
  3. 不同请求可以分别保存。
  4. 不同用户的请求不会相互混淆。

不调用模型,不消耗 API 额度,也不修改之前的模拟订单数据库。

1. 新建测试文件

在上一课文件所在目录,新建:

复制代码
C:\Users\Administrator\langchain-study\test_unit5_lesson10_idempotency.py

写入:

复制代码
import sqlite3
import unittest

from contextlib import closing
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch

import unit5_lesson9_idempotency as orders


class TestIdempotency(unittest.TestCase):

    def setUp(self):
        # 每个测试使用独立的临时目录
        self.temp_dir = TemporaryDirectory()
        self.addCleanup(self.temp_dir.cleanup)

        self.db_path = Path(self.temp_dir.name) / "test_orders.sqlite"

        # 临时替换上一课模块里的数据库路径
        patcher = patch.object(orders, "DB_PATH", self.db_path)
        patcher.start()
        self.addCleanup(patcher.stop)

    def save(
        self,
        request_id="request-1",
        quantity=2,
        total_yuan="81.00",
        user_id="A",
    ):
        """测试辅助函数,减少重复填写参数。"""
        return orders.save_simulated_order(
            user_id=user_id,
            request_id=request_id,
            book_name="三体",
            quantity=quantity,
            total_yuan=total_yuan,
        )

    def test_same_request_is_saved_once(self):
        first_id, first_created = self.save()
        second_id, second_created = self.save()

        self.assertTrue(first_created)
        self.assertFalse(second_created)
        self.assertEqual(first_id, second_id)

        # 不仅检查返回值,也检查数据库实际记录数
        with closing(sqlite3.connect(self.db_path)) as conn:
            count = conn.execute(
                "SELECT COUNT(*) FROM simulated_orders"
            ).fetchone()[0]

        self.assertEqual(count, 1)

    def test_changed_payload_is_rejected(self):
        original_id, _ = self.save()

        # 请求编号不变,却把2本改成3本,必须拒绝
        with self.assertRaises(ValueError):
            self.save(quantity=3, total_yuan="121.50")

        # 冲突发生后,原记录仍应保持不变
        saved_id, created = self.save()

        self.assertEqual(saved_id, original_id)
        self.assertFalse(created)

    def test_different_requests_create_different_records(self):
        first_id, first_created = self.save(request_id="request-1")
        second_id, second_created = self.save(request_id="request-2")

        self.assertTrue(first_created)
        self.assertTrue(second_created)
        self.assertNotEqual(first_id, second_id)

    def test_same_request_id_for_different_users(self):
        first_id, first_created = self.save(user_id="A")
        second_id, second_created = self.save(user_id="B")

        self.assertTrue(first_created)
        self.assertTrue(second_created)
        self.assertNotEqual(first_id, second_id)


if __name__ == "__main__":
    unittest.main(verbosity=2)

最后一个测试只验证记录按用户区分,不验证会员价格;这里的金额是固定测试数据。

2. 运行测试

在项目目录执行:

复制代码
python test_unit5_lesson10_idempotency.py

如果上一课的保存函数保持原样,预期看到四个测试都显示 ok,最后类似:

复制代码
Ran 4 tests in ...s

OK

含义是:这四个测试场景通过了。

如果出现:

  • FAIL:运行结果不符合断言。
  • ERROR:测试过程中出现了未预期的异常。

3. 看懂三个主要写法

① setUp():每个测试开始前准备环境

复制代码
def setUp(self):

它不是整个文件只执行一次,而是每个 test_... 方法开始前执行一次。因此四个测试使用四份独立的临时数据库,不依赖执行顺序。Python 测试框架说明

② assert...:把预期写成检查

复制代码
self.assertEqual(first_id, second_id)

意思是"两个编号必须相同"。

复制代码
self.assertFalse(second_created)

意思是"第二次不能新建记录"。

复制代码
with self.assertRaises(ValueError):
    self.save(quantity=3, total_yuan="121.50")

意思是"这里应该抛出 ValueError"。如果没有抛出,测试反而失败。

③ patch.object():临时替换配置

复制代码
patch.object(orders, "DB_PATH", self.db_path)

它临时把保存函数使用的数据库路径换成测试路径,不会修改上一课的源文件 。测试清理时恢复原值。官方说明

这里仍然执行真实的 SQLite 写入,只是写入临时数据库,并没有把保存函数换成假的。

4. 为什么不直接让模型测试?

因为我们要验证的是一个确定的业务规则:

同一个请求是否重复保存?

这件事可以直接调用函数,检查返回值和数据库记录,不需要经过模型。这样测试更快,也不会受模型措辞或工具选择波动影响。

但这四个测试还没有覆盖:

  • 模型是否选择正确的工具。
  • 人工拒绝后是否确实不写入。
  • 参数校验是否正确。
  • 并发请求、断电或跨进程故障。

因此,OK 表示这些已写出的场景通过,不能理解成整个助手已经没有问题。

本课记住:模型行为需要评估,确定的业务逻辑则应该用自动化测试检查。

相关推荐
阿义有问题13 分钟前
MySQL 迁移达梦全流程实战:DTS 七步图解 + 对象名全大写适配(附踩坑清单)
数据库
七夜zippoe1 小时前
Agent 无限循环烧 Token:死循环检测与中断机制设计
ai·agent·token·死循环·中断机制
猫猫不是喵喵.3 小时前
从0到1创建一个微服务项目
java·数据库·微服务·1024程序员节
曲径海盗3 小时前
Try-Except怎么用,3个追问解答在自动化下的应用
java·jvm·oracle
AI算力3 小时前
私有云时代来临:AI NAS如何重塑你的数字生活?
人工智能·科技·ai·生活·nas·ai算力·ainas
lsc_blog4 小时前
听到优化风声,先准备能由自己掌控的事
数据库·分布式·中间件·职场和发展·重构
Flynn Liang4 小时前
Rerank 与上下文工程:重排、压缩与引用溯源的收益边界
langchain·rag·langchain4j·rerank·上下文工程·引用溯源
墨痕诉清风4 小时前
SSRF漏洞实战记录+OSS桶命令执行+MCP漏洞
linux·服务器·数据库
Ashe8137 小时前
MySQL 慢日志先别急着加索引:三个指标能拦掉大半误判
数据库
dozenyaoyida7 小时前
AI与大模型新闻日报 | 2026-11-12
人工智能·ai·大模型·新闻