尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Phoenix Context 与 Schema 测试完全指南:DataCase、SQL Sandbox 与测试生成机制

Phoenix Context 与 Schema 测试完全指南:DataCase、SQL Sandbox 与测试生成机制 Phoenix Context 与 Schema 测试完全指南DataCase、SQL Sandbox 与测试生成机制【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix本篇技术指南以 Phoenix 官方测试指南为骨架系统讲解生成器为 Context 与 Schema 自动生成的测试代码如何运作从mix phx.gen.html产出的blog_test.exs入手深入剖析DataCase测试基座、SQL Sandbox 事务隔离原理、async: true并发加速的适用场景以及何时该直接测试 Schema、如何用errors_on/1验证 changeset 校验规则。读完你将掌握在 Phoenix 应用中为数据层编写可靠、可并行、可维护测试的完整实战方案。前置准备与测试起点本文假定你已经完成 安装指南 并让应用正常运行起来通读过 Introduction to Testing理解 ExUnit 断言、ConnCase与基础测试结构了解 Contexts 指南 中关于 Context 与 Schema 职责划分的约定。在《Introduction to Testing》的末尾我们通过下面的命令为 posts 生成了一个完整的 HTML 资源$ mix phx.gen.html Blog Post posts title body:text这条命令免费为我们产出了一系列模块BlogContext、PostSchema以及各自对应的测试文件。回顾 Context 指南中的定义Blog Context 只是面向业务领域某一区域的函数集合而 Post Schema 则映射到数据库中的一张特定表。Context 负责编排创建、更新、写库、对接 APISchema 负责描述数据结构和约束。在深入任何细节之前先运行一次完整测试套件确认一切干净$ mix test ................ Finished in 0.6 seconds 21 tests, 0 failures Randomized with seed 63841421 个测试全部通过。注意输出末尾的Randomized with seedExUnit 默认随机化测试执行顺序这正是保证测试相互隔离、避免恰好因为某个顺序才通过的手段具体讨论见 testing.md。解读自动生成的 Context 测试文件打开生成的test/hello/blog_test.exs对应生成模板 context_test.exs.eex其骨架如下defmodule Hello.BlogTest do use Hello.DataCase alias Hello.Blog describe posts do alias Hello.Blog.Post import Hello.BlogFixtures invalid_attrs %{body: nil, title: nil} test list_posts/0 returns all posts do post post_fixture() assert Blog.list_posts() [post] end ...逐行拆解use Hello.DataCase文件顶部导入DataCase。它与HelloWeb.ConnCase相似但职责不同——ConnCase提供面向 HTTP 连接controller/view 测试的辅助设施而DataCase提供面向 Context 与 Schema 的辅助设施Repo 别名、Ecto 导入、SQL Sandbox 隔离。alias Hello.Blog将Hello.Blog简写为Blog后续测试直接调用Blog.list_posts()等 Context 函数。describe posts块这是 ExUnit 的测试分组特性。之所以按资源名分组是因为Phoenix Context 可以容纳多个 Schema每个 Schema 对应一个describe块同一 Context 的测试全部收敛在一个文件中。describe 块一个 Context 承载多个 Schema如果继续执行$ mix phx.gen.html Blog Comment comments post_id:references:posts body:textHello.BlogContext 中会新增一批 Comment 相关函数而测试文件里会多出一个全新的describe comments块。也就是说测试文件的结构天然反映 Context 的聚合边界——这是理解 Phoenix 测试组织方式的关键。生成测试的完整断言模式从模板 test_cases.exs.eex 可以看出每个describe块为 Context 的每个 CRUD 函数生成一个直白的测试模式高度一致调用 Context 函数对结果做断言必要时先用 fixture 造数据。以create_post/1为例test create_post/1 with valid data creates a post do valid_attrs %{body: some body, title: some title} assert {:ok, %Post{} post} Blog.create_post(valid_attrs) assert post.body some body assert post.title some title endassert {:ok, %Post{} post} Blog.create_post(valid_attrs)是一个典型的三合一断言先验证返回{:ok, ...}元组再通过%Post{}模式匹配确认结构是Post同时把记录绑定到post变量供后续字段断言使用。模板生成的其他测试还包括get_post!/1按 id 取回记录并与 fixture 相等比较create_post/1 with invalid data用invalid_attrs所有字段为nil断言返回{:error, %Ecto.Changeset{}}update_post/2更新后逐字段断言新值且用invalid_attrs更新时验证返回 error changeset、原记录不变delete_post/1删除后断言{:ok, %Post{}}并验证再次查询抛出Ecto.NoResultsErrorchange_post/1返回%Ecto.Changeset{}即可。关于 fixture测试开头import Hello.BlogFixtures引入的post_fixture/1由模板 fixtures.ex.eex 生成其内部逻辑是用默认参数如title: some title、body: some body调用Blog.create_post/1并返回结构体因此它能与上述断言模式无缝配合。此时一个关键问题浮现测试写入数据库的数据如何确保不影响其他测试答案就在DataCase与 SQL Sandbox 中。DataCaseContext 与 Schema 测试的基座打开test/support/data_case.ex即生成模板 data_case.ex.eex 渲染后的产物完整内容如下defmodule Hello.DataCase do use ExUnit.CaseTemplate using do quote do alias Hello.Repo import Ecto import Ecto.Changeset import Ecto.Query import Hello.DataCase end end setup tags do Hello.DataCase.setup_sandbox(tags) :ok end def setup_sandbox(tags) do pid Ecto.Adapters.SQL.Sandbox.start_owner!(Hello.Repo, shared: not tags[:async]) on_exit(fn - Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) end def errors_on(changeset) do ... end end三个组成部分各司其职use ExUnit.CaseTemplate这是 ExUnit 提供的用例模板机制让use Hello.DataCase取代内置的use ExUnit.Case。它与ConnCase的实现方式完全一致。using回调向所有使用DataCase的测试模块注入代码——alias Hello.Repo、import Ecto~w等查询构造、import Ecto.Changesetcast/3、validate_required/2等、import Ecto.Queryfrom、where等、import Hello.DataCaseerrors_on/1等辅助函数。setup块每个测试执行前调用setup_sandbox(tags)核心是启动 SQL Sandbox。注意setup返回:ok而非ConnCase那样的{:ok, conn: ...}——数据层测试不需要连接元数据只需隔离数据库状态。SQL Sandbox事务隔离与自动回滚setup_sandbox/1的关键调用链pid Ecto.Adapters.SQL.Sandbox.start_owner!(Hello.Repo, shared: not tags[:async]) on_exit(fn - Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)SQL Sandbox 正是测试写库互不影响的机制每个测试开始时在数据库里开启一个事务测试结束时自动回滚该测试创建的所有数据被抹除。因此无论测试执行顺序如何随机化每个测试看到的数据库都是干净的起点。实现细节上tags[:async]决定了沙箱的所有权模式同步测试async: false默认shared: true沙箱以共享模式运行异步测试async: trueshared: false每个测试独占沙箱连接。而test/test_helper.exs中通常有一行见 testing.md 的说明Ecto.Adapters.SQL.Sandbox.mode(Hello.Repo, :manual)它把 Repo 切换到 manual 模式由每个测试用例自己管理沙箱生命周期——这就是setup_sandbox/1中start_owner!/stop_owner成对出现的原因。async: true并发加速的正确姿势SQL Sandbox 的另一个能力是让多个测试并发运行即使它们都在读写数据库。这对 PostgreSQL 数据库原生支持可显著加快 Context 与 Controller 测试use Hello.DataCase, async: true使用异步沙箱时有几点必须注意并发隔离的前提是每个测试通过start_owner!获取独立的数据库连接异步测试不能依赖进程外、跨测试共享的状态例如全局Agent、ETS、GenServer 状态不同数据库适配器对并发的支持不同模板源码 data_case.ex.eex 的moduledoc明确提示PostgreSQL 可开启async: true其他数据库不建议更多细节请查阅Ecto.Adapters.SQL.Sandbox的官方文档该模块即setup_sandbox/1所调用的实现。errors_on/1把 changeset 错误转成可断言的 MapDataCase模块末尾定义了errors_on/1模板中的完整实现为def errors_on(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {message, opts} - Regex.replace(~r%{(\w)}, message, fn _, key - opts | Keyword.get(String.to_existing_atom(key), key) | to_string() end) end) end它的作用是把一个 changeset 的错误集合转换成字段 → 错误消息列表的 Map并展开消息中的%{...}占位符如should be at least %{count} character(s)会被渲染成should be at least 2 character(s)。这使得断言可以这样写assert %{title: [should be at least 2 character(s)]} errors_on(changeset)它是测试 Schema 校验规则的核心辅助函数下面正式介绍 Schema 测试。何时测 Context、何时测 Schema生成 HTML Post 资源时Phoenix 为 Context 生成了测试文件但没有为 Schema 生成测试文件。这并非意味着 Schema 不需要测试而是到目前为止还不需要。判断标准与代码应该放 Context 还是 Schema是同一个问题。社区约定如下所有无副作用的代码放进 Schema纯数据结构操作、schema 定义、changeset 与校验逻辑都属于 Schema 的职责应直接在 Schema 测试中覆盖Context 承载有副作用的代码创建/更新 Schema、写数据库或对接外部 API这些是 Context 的职责由 Context 测试覆盖。基于这个划分我们的 Schema 只差校验逻辑还没被测试覆盖正好补写 Schema 专属测试。给 Schema 增加校验并编写测试先给lib/hello/blog/post.ex的changeset/2增加一条规则——标题至少 2 个字符def changeset(post, attrs) do post | cast(attrs, [:title, :body]) | validate_required([:title, :body]) | validate_length(:title, min: 2) end然后在test/hello/blog/post_test.exs新建测试模块defmodule Hello.Blog.PostTest do use Hello.DataCase, async: true alias Hello.Blog.Post test title must be at least two characters long do changeset Post.changeset(%Post{}, %{title: I}) assert %{title: [should be at least 2 character(s)]} errors_on(changeset) end end这个测试有两个值得注意的点use Hello.DataCase, async: trueSchema 测试只做内存中的 changeset 构造不触碰数据库因此可以直接异步运行以加速套件断言模式直接调用Post.changeset/2构造 changeset再用errors_on/1把错误 Map 化并精确匹配。validate_length(:title, min: 2)对应的默认错误消息就是should be at least 2 character(s)含被errors_on/1展开的占位符。运行验证$ mix test test/hello/blog/post_test.exs随着业务领域增长Context 测试与 Schema 测试各自有了清晰的归宿Context 测试验证领域编排与数据持久化Schema 测试验证数据结构的约束与校验二者互补、互不混淆。结合生成模板理解测试代码从何而来为了更透彻地掌握这些测试值得回到仓库的生成器模板它们就是mix phx.gen.html产出测试代码的源头priv/templates/phx.gen.context/context_test.exs.eex仅 4 行声明测试模块并使用DataCase其余内容由下一模板注入priv/templates/phx.gen.context/test_cases.exs.eex生成describe块与全部 CRUD 测试包括invalid_attrs的构造把schema.params.create中所有字段置为nil、list_/get_/create_/update_/delete_/change_系列断言priv/templates/phx.gen.context/fixtures.ex.eex生成xxx_fixture/1函数内部通过Enum.into合并默认参数并调用Blog.create_xxx/1installer/templates/phx_ecto/data_case.ex.eexDataCase的源头模板其adapter_config[:test_setup]占位符会根据数据库适配器PostgreSQL/MySQL/SQLite3 等渲染出对应的沙箱启动代码。换言之mix phx.gen.html Blog Post posts title body:text生成 21 个测试这件事本身是模板驱动的理解模板即可预测任意资源的测试形态在 integration_test 目录下还有针对多种数据库适配器postgres、mysql、mssql、sqlite3的端到端生成验证测试可印证模板对各类适配器的兼容性。小结至此围绕 Phoenix 的 Context 与 Schema 测试我们完成了从生成的测试长什么样到底层如何保证隔离再到如何补写自己的测试的闭环mix phx.gen.html按 Context 聚合生成测试每个 Schema 一个describe块断言模式与 test_cases.exs.eex 模板一一对应DataCase通过ExUnit.CaseTemplate注入 Repo 别名与 Ecto 导入并在setup阶段启动 SQL SandboxSQL Sandbox 用每测试一个事务、结束即回滚的方式保证测试间数据隔离PostgreSQL 下可用async: true并发加速其他适配器需谨慎无副作用的校验逻辑放 Schema用errors_on/1直接断言 changeset 错误有副作用的数据编排放 Context用 fixture Context 函数断言结果。继续深入可阅读姊妹篇 Testing Controllers 与 Testing Channels将这一套Case 模板 Sandbox 生成测试的心智模型扩展到整个请求/连接层。【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表