ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ADK Python 单元测试风格指南:从测试命名到 AAA 结构的十项铁律

ADK Python 单元测试风格指南:从测试命名到 AAA 结构的十项铁律 ADK Python 单元测试风格指南从测试命名到 AAA 结构的十项铁律【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python导读本文基于 adk-python 仓库中的 ADK Testing Style Guide系统梳理 Agent Development Kitgoogle-adk官方约定的单元测试编写规范。这套规范覆盖测试命名、Docstring、断言写法、Fixture 设计、Arrange-Act-Assert 结构以及测试文件组织方式并直接约束src/google/adk/与tests/unittests/下的全部测试代码多数条目由 pre-commit 钩子或 CI 任务强制检查。读完本文你将掌握一套「通过公开接口、测试行为而非实现、重构免疫」的测试方法论并能在自己的 ADK 项目中直接套用官方测试模板。核心原则三条永不妥协的基线原文档开篇即立下三条核心原则它们决定了后续每一条规则的取舍方向Test through the public interface通过公开接口测试——用户怎么调用测试就怎么调用用户能看到什么测试就断言什么。不钻进私有方法、私有属性里做内部审计。Test behavior, not implementation测试行为而非实现——验证的是输出结果、副作用与错误而不是内部机制如何运作。Refactor-proof重构免疫——只要一次内部重构没有改变对外行为所有测试就应该原封不动地继续通过。这三条原则在实践中互相支撑命名规则规则 1、2保证了测试描述的是调用者观察到的东西禁止触碰内部状态规则 4保证了重构免疫断言讲故事规则 8保证了测试读起来像一份规格说明书specification而不是实现复述。测试运行基础设施asyncio_mode auto仓库的 pytest 配置直接写死了异步测试的运行方式。在 pyproject.toml 中[tool.pytest] ini_options.testpaths [ tests ] ini_options.asyncio_default_fixture_loop_scope function ini_options.asyncio_mode auto在asyncio_mode auto下一个裸的async def test_...函数即可直接运行无需pytest.mark.asyncio装饰器。原文档特别指出仓库中许多较老的测试仍带着这个标记它无害也不值得为此做一轮清理。此外该配置还设定了asyncio_default_fixture_loop_scope function即异步 fixture 默认按函数级作用域管理其事件循环。如果不想依赖全局 auto 模式也可以改为显式标记每个测试但这会增加样板代码——这也是官方选择 auto 模式的原因。规则一测试名描述行为而非机制测试名是测试的第一份文档。命名应当回答调用者观察到了什么而不是代码内部发生了什么# Good —— 描述调用者观察到的东西 def test_empty_queue_returns_none(): def test_retry_stops_after_max_attempts(): def test_missing_key_raises_key_error(): # Bad —— 描述实现细节 def test_deque_popleft_called(): def test_retry_counter_incremented(): def test_dict_getitem_raises():对比两组命名即可看出test_deque_popleft_called一旦把deque换成别的容器就失去意义而test_empty_queue_returns_none无论底层如何实现都成立。规则二Docstring——首行一句话复杂测试再配 Setup/Act/Assert 分解Docstring 的第一行必须从调用者视角描述期望行为。对于多步骤、多轮调用的复杂测试再追加结构化的 Setup / Act / Assert 分解# Good —— 简单测试一句话足够 Getting from an empty cache returns the default value. # Good —— 复杂测试配结构化分解 Partial FR re-runs nested Workflow, resolved child completes while unresolved stays interrupted. Setup: outer_wf → inner_wf → (child_a, child_b) → join. Both children interrupt on first run. Act: - Run 2: resolve only child_as FR. - Run 3: resolve child_bs FR. Assert: - Run 2: child_a produces output, invocation still interrupted. - Run 3: child_b produces output, join completes, no interrupts. # Bad —— 复述实现 LRUCache._store.get returns sentinel when key missing. ThreadPool._accept_tasks flag checked in submit().规则一与规则二是一体两面命名与 Docstring 都在回答这个测试在验证什么外部行为。第二则 Bad 示例直接点名私有属性LRUCache._store与内部标志位_accept_tasks这正是规则四要杜绝的写法。规则三每个测试只覆盖一个行为如果一个测试同时检查多个互不相关的行为就拆开它。判断标准很朴素如果你没法用一句话描述这个测试在测什么它就测得太多了。# Bad —— 一个测试同时测了容量、逐出、默认值 def test_cache_behavior(): assert cache.size 0 assert cache.get(x) is None cache.put(a, 1) assert cache.size 1 # Good —— 拆成聚焦的独立测试 def test_new_cache_is_empty(): A freshly created cache has no entries. def test_cache_evicts_oldest_when_full(): Adding to a full cache removes the least recently used entry.单一行为的测试失败时失败信息直接指向具体契约如满缓存逐出最久未用条目而不是让排查者在一堆断言里猜哪个先崩。规则四不测试内部状态私有属性、内部状态机字段、私有类型判断都属于实现细节一律不测# Bad —— 伸手进私有属性 assert pool._workers[0].is_alive assert parser._state HEADER assert isinstance(router._handler, _FastHandler) # Good —— 通过公开接口测试 assert pool.active_count 1 assert parser.parse(data) expected assert router.route(/api) handler这条规则是重构免疫的直接保障只要对外可见的active_count、parse()、route()行为不变内部把_workers换成别的数据结构、把状态机改成别的实现测试都无须改动。规则五尽量用真实组件只在边界处 mockADK 测试的基本取向是尽可能使用真实实现mock 只留给真正的边界需要 mock 的外部依赖LLM API、云服务、会话存储session stores。使用真实 ADK 组件BaseNode子类、Event、Context。需要 mock 的边界测试NodeRunner时 mockInvocationContext。这与仓库中工作流测试指南 adk-agent-builder 测试参考 的取向一致——那里同样强调用InMemoryRunner 真实Workflow组件做端到端行为验证而不是 mock 掉被测组件本身。在 mock 问题上的另一个佐证是该指南中给 LLM 模型造假时直接继承BaseLlm实现唯一的抽象方法generate_content_async而不是去 mock 整个模型接口。规则六Fixture 保持最小化Fixture 只做触发目标行为所需的最简设置一个 fixture 一个用途# Good —— 最小 fixture单一用途 def make_user(roleviewer): return User(nametest, emailtt.com, rolerole) # Bad —— 大杂烩 fixture混入无关设置 def make_full_test_env(): db create_database() user create_user_with_billing() setup_notifications() ...大杂烩 fixture 的问题在于它把多个场景的公共与个性设置焊在一起任何测试修改它都会波及其他用例最终无人敢动、逐渐腐烂。规则七Arrange 逻辑就近摆放当一个辅助类或 fixture 只被一个测试使用就直接定义在该测试函数内部。这样设置逻辑在使用点可见读者无需滚动到数百行之外的模块级定义处来回对照# Good —— 辅助类内联在测试旁边 async def test_state_delta_bundled_with_output(): State set before yield is flushed onto the output event. class _Node(BaseNode): async def _run_impl(self, *, ctx, node_input): ctx.state[color] blue yield result ctx, events _make_ctx() await NodeRunner(node_Node(namen), parent_ctxctx).run() assert events[0].output result assert events[0].actions.state_delta[color] blue # Bad —— 辅助类定义在 300 行之外读者需要来回滚动 class _StateThenOutputNode(BaseNode): async def _run_impl(self, *, ctx, node_input): ctx.state[color] blue yield result # ... 300 行之后 ... async def test_state_delta_bundled_with_output(): node _StateThenOutputNode(namen) ...取舍标准很明确当 3 个及以上测试共享同一个辅助时才把它提取到模块级。内联类以_Node这种下划线前缀命名也符合 visibility 规范 中私有符号不加进公开 API的一贯风格。规则八断言要讲故事断言应当读起来像一份规格说明而不是防御性地测试框架本身# Good —— 读起来像规格 assert queue.size 0 assert config.get(timeout) 30 assert response.status_code 404 # Bad —— 过度防御测试的是框架行为 assert isinstance(queue, Queue) assert hasattr(config, get) assert len(response.headers) 0assert isinstance(queue, Queue)这类断言在类型错之外几乎不会失败它提供的信息量约等于零却会妨碍真实行为断言的修改——这正是规则三一句话说不清就别写的延伸。规则九按 Arrange、Act、Assert 组织测试每个测试都应包含三个边界清晰的步骤Arrange——搭建该场景特有的外部状态。被众多测试共享的通用设置放在 fixture 里。Act——调用被测系统通常是一次调用。Assert——验证返回值或可见的状态变化不再调用被测系统。步骤之间用空行分隔保持视觉区分。简单测试每步只有一条语句可以省略空行复杂测试则用描述性注释标注阶段例如 Given [situation] / When [action] / Then [expectation]——避免使用不提供任何信息的裸标签# Good —— 清晰的视觉分隔 def test_cache_returns_stored_value(): cache Cache() cache.put(key, value) result cache.get(key) assert result value # Good —— 简单测试省略空行 def test_new_cache_is_empty(): assert Cache().size 0 # Bad —— 步骤交错 def test_cache_behavior(): cache Cache() cache.put(key, value) result cache.get(key) assert result value cache.put(key2, value2) # more setup after assert assert cache.size 2Bad 示例中最致命的点在于assert之后还追加了cache.put(key2, value2)——在断言之后继续设置状态会让测试的执行顺序与阅读顺序脱节。仓库中大量测试遵循了这一 Given/When/Then 注释风格例如tests/unittests/a2a/executor/test_task_result_aggregator.py中的# Then process failed - should override注释以及tests/unittests/a2a/executor/test_a2a_agent_executor.py中的# When aggregator state is working but no message, final event should be working等写法。规则十按被测单元组织测试文件而不是按变更组织新测试应加入被测模块/功能已有的测试文件。永远不要创建以 CL变更列表、bug 或某次改动命名的测试文件——这会把一个模块的覆盖拆散到多个文件里并很快腐烂因为下一个编辑该模块的人会去模块对应的文件里找测试而不会去翻那个以一次性清理命名的文件。在新建文件之前先寻找现有归属test_module*.py。ADK 已经按关注点拆分了一些模块因此归属可能是功能特定的# Bad —— 以变更命名把 llm_agent/runner/llm_request 的覆盖 # 拆进一个无人维护的大杂烩文件 tests/unittests/agents/test_improved_error_messages.py # Good —— 每个测试都落在被测单元已有的文件里 tests/unittests/agents/test_llm_agent_error_messages.py # LlmAgent messages tests/unittests/models/test_llm_request.py # LlmRequest messages tests/unittests/test_runners.py # Runner messages如果兄弟测试已经断言了相同行为就扩展那个断言而不是在新文件里重复。只有真正全新的模块或功能领域才允许创建新文件命名格式为test_module_feature.py。这一约定在仓库中有直接印证LlmAgent的错误消息测试确实位于 test_llm_agent_error_messages.py而 runner 相关的测试集中在 test_runners.py 以及 runners 目录 下按行为拆分的文件如test_runner_node.py、test_runner_rewind.py。tests/unittests/agents目录下test_llm_agent_callbacks.py、test_llm_agent_interruptions.py、test_llm_agent_streaming_output.py等按功能面拆分的文件正是模块内按关注点拆分、但始终围绕被测单元组织的范例。官方测试结构模板将以上规则落成一个可复制的文件模板Tests for ComponentName. Verifies that component correctly high-level behavior. # --- Fixtures (minimal, one purpose each) --- def _make_service(): ... # --- Tests (one behavior per test) --- def test_behavior_description(): One sentence: what the system does from the outside # Given a service with default config service _make_service() input_data hello # When the operation is performed result service.do_something(input_data) # Then the result matches expectations assert result expected模板的构成要素与前述规则一一对应文件级 Docstring 说明被测组件与高层行为_make_service式的最小 fixture 只服务一个目的测试名遵循行为描述函数内 Docstring 一句话点明外部可见行为Given/When/Then 注释标注 AAA 三段。与其他规范的衔接这套测试规范不是孤立的它与 adk-style 技能簇中其他参考文档配合使用文件放置新.py文件放哪里、license 头怎么写、测试文件放哪里、怎么命名见 file-organization.md测试文件组织是其中的组成部分。风格总览本指南是 adk-style 技能 下编写或重构单元测试时应查阅的参考该技能同时覆盖可见性、导入、类型标注、Pydantic v2、格式化、Docstring、日志、异步 I/O 等主题。技能说明中明确指出这些规范多数由 pre-commit 钩子或 CI 任务强制违规会阻塞 PR 而非等到评审时才被发现。工作流测试如果被测对象是 Workflow 等高层组件adk-agent-builder 测试参考 提供了InMemoryRunner驱动的端到端测试范式并给出避免 flaky 的具体习惯如每个测试独享一个 runner 与会话、app_name保持唯一避免并行冲突、用event.is_final_response()过滤最终答复等与本指南的行为导向测试理念完全一致。总结ADK 测试风格指南的全部内容可以浓缩为一句话测试是写给未来读者看的规格说明书而不是当前实现的记录。从命名、Docstring 到断言、Fixture再到文件组织每一条规则都在把测试从实现细节的镜像推向外部契约的验证。遵循这套规范写出的测试能在内部重构发生时保持绿色、在失败时给出明确的契约信号也让任何一个后来者都能根据test_module_feature.py的命名快速找到每个单元的全部测试。【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表