查看源代码 测试简介
测试已成为软件开发过程不可或缺的一部分,轻松编写有意义的测试的能力是任何现代 Web 框架必不可少的特性。Phoenix 对此非常重视,提供支持文件,使框架的所有主要组件易于测试。它还会在任何生成的模块旁边生成包含真实示例的测试模块,以帮助我们开始。
Elixir 附带一个名为 ExUnit 的内置测试框架。ExUnit 努力保持清晰明了,将魔法降至最低。Phoenix 使用 ExUnit 进行所有测试,我们也将在这里使用它。
运行测试
当 Phoenix 为我们生成 Web 应用程序时,它还包括测试。要运行它们,只需键入 mix test
$ mix test
....
Finished in 0.09 seconds
5 tests, 0 failures
Randomized with seed 652656
我们已经有五个测试了!
事实上,我们已经拥有一个完全为测试设置好的目录结构,包括测试助手和支持文件。
test
├── hello_web
│ └── controllers
│ ├── error_html_test.exs
│ ├── error_json_test.exs
│ └── page_controller_test.exs
├── support
│ ├── conn_case.ex
│ └── data_case.ex
└── test_helper.exs
我们免费获得的测试用例包括来自 test/hello_web/controllers/
的测试用例。它们正在测试我们的控制器和视图。如果您还没有阅读控制器和视图的指南,现在是好时机。
理解测试模块
我们将使用接下来的部分来熟悉 Phoenix 测试结构。我们将从 Phoenix 生成的三个测试文件开始。
我们将要查看的第一个测试文件是 test/hello_web/controllers/page_controller_test.exs
。
defmodule HelloWeb.PageControllerTest do
use HelloWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
end
这里发生了几件有趣的事情。
我们的测试文件只是定义模块。在每个模块的顶部,您会找到一行,例如
use HelloWeb.ConnCase
如果您要编写一个 Elixir 库,而不是 Phoenix,您将编写 use ExUnit.Case
而不是 use HelloWeb.ConnCase
。但是,Phoenix 已经附带了一堆用于测试控制器的功能,而 HelloWeb.ConnCase
构建在 ExUnit.Case
之上,以引入这些功能。我们很快就会探索 HelloWeb.ConnCase
模块。
然后我们使用 test/3
宏定义每个测试。 test/3
宏接收三个参数:测试名称、我们正在模式匹配的测试上下文以及测试的内容。在这个测试中,我们通过在路径 “/” 上对 “GET” HTTP 请求使用 get/2
宏访问应用程序的根页面。然后我们断言渲染的页面包含字符串 “Peace of mind from prototype to production”。
在 Elixir 中编写测试时,我们使用断言来检查某件事是否为真。在我们的例子中,assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
做了几件事
- 它断言
conn
已渲染响应 - 它断言响应具有 200 状态码(在 HTTP 术语中表示 OK)
- 它断言响应的类型是 HTML
- 它断言
html_response(conn, 200)
的结果(一个 HTML 响应)包含字符串 “Peace of mind from prototype to production”
但是,我们用在 get
和 html_response
上的 conn
来自哪里?要回答这个问题,让我们看一下 HelloWeb.ConnCase
。
ConnCase
如果您打开 test/support/conn_case.ex
,您会发现它(注释已删除)
defmodule HelloWeb.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
# The default endpoint for testing
@endpoint HelloWeb.Endpoint
use HelloWeb, :verified_routes
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import HelloWeb.ConnCase
end
end
setup tags do
Hello.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
这里有很多东西需要解释。
第二行说这是一个案例模板。这是一个 ExUnit 功能,允许开发人员用他们自己的案例替换内置的 use ExUnit.Case
。这一行几乎就是我们能够在控制器测试的顶部编写 use HelloWeb.ConnCase
的原因。
现在我们已经将这个模块变成了一个案例模板,我们可以定义在特定情况下调用的回调。 using
回调定义了要在调用 use HelloWeb.ConnCase
的每个模块上注入的代码。在本例中,它首先使用我们的端点的名称设置 @endpoint
模块属性。
接下来,它将 :verified_routes
连接起来,以允许我们在测试中使用基于 ~p
的路径,就像我们在应用程序的其他部分一样,以便在测试中轻松生成路径和 URL。
最后,我们导入 Plug.Conn
,这样控制器中可用的所有连接助手在测试中也可用,然后导入 Phoenix.ConnTest
。您可以查阅这些模块以了解所有可用功能。
然后我们的案例模板定义一个 setup
块。 setup
块将在测试之前被调用。大部分设置块用于设置 SQL 沙箱,我们将在后面讨论。在 setup
块的最后一行,我们会发现
{:ok, conn: Phoenix.ConnTest.build_conn()}
setup
的最后一行可以返回测试元数据,这些元数据将在每个测试中可用。我们在这里传递的元数据是一个新构建的 Plug.Conn
。在我们的测试中,我们在测试的开头从这个元数据中提取连接
test "GET /", %{conn: conn} do
这就是连接的来源!起初,测试结构确实带有一些间接性,但随着测试套件的增长,这种间接性会得到回报,因为它允许我们减少样板代码的数量。
视图测试
应用程序中的其他测试文件负责测试我们的视图。
错误视图测试用例 test/hello_web/controllers/error_html_test.exs
说明了它自身的一些有趣之处。
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
# Bring render_to_string/4 for testing custom views
import Phoenix.Template
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorHTML, "404", "html", []) == "Not Found"
end
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
end
end
HelloWeb.ErrorHTMLTest
设置了 async: true
,这意味着这个测试用例将与其他测试用例并行运行。虽然案例内的单个测试仍然按顺序运行,但这可以大大提高整体测试速度。
它还导入 Phoenix.Template
以使用 render_to_string/4
函数。有了它,所有断言都可以是简单的字符串相等性测试。
按目录/文件运行测试
现在我们已经了解了测试的功能,让我们看看运行它们的几种方法。
正如我们在本指南的开头看到的那样,我们可以使用 mix test
运行整个测试套件。
$ mix test
....
Finished in 0.2 seconds
5 tests, 0 failures
Randomized with seed 540755
如果我们想运行给定目录中的所有测试(例如 test/hello_web/controllers
),我们可以将该目录的路径传递给 mix test
。
$ mix test test/hello_web/controllers/
.
Finished in 0.2 seconds
5 tests, 0 failures
Randomized with seed 652376
为了运行特定文件中的所有测试,我们可以将该文件的路径传递给 mix test
。
$ mix test test/hello_web/controllers/error_html_test.exs
...
Finished in 0.2 seconds
2 tests, 0 failures
Randomized with seed 220535
我们可以通过将冒号和行号附加到文件名来运行文件中单个测试。
假设我们只想运行 HelloWeb.ErrorHTML
渲染 500.html
的方式的测试。该测试从文件的第 11 行开始,所以我们可以这样做。
$ mix test test/hello_web/controllers/error_html_test.exs:11
Including tags: [line: "11"]
Excluding tags: [:test]
.
Finished in 0.1 seconds
2 tests, 0 failures, 1 excluded
Randomized with seed 288117
我们选择通过指定测试的第一行来运行它,但实际上,该测试的任何一行都可以。这些行号都将起作用 - :11
、:12
或 :13
。
使用标签运行测试
ExUnit 允许我们分别或为整个模块标记我们的测试。然后,我们可以选择只运行具有特定标签的测试,或者我们可以排除具有该标签的测试并运行所有其他测试。
让我们尝试一下它是如何工作的。
首先,我们将添加一个 @moduletag
到 test/hello_web/controllers/error_html_test.exs
。
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
@moduletag :error_view_case
...
end
如果我们只使用一个原子作为模块标签,ExUnit 假设它的值为 true
。如果需要,我们也可以指定其他值。
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
@moduletag error_view_case: "some_interesting_value"
...
end
现在,让我们把它作为一个简单的原子 @moduletag :error_view_case
。
我们可以通过将 --only error_view_case
传递给 mix test
来只运行错误视图案例中的测试。
$ mix test --only error_view_case
Including tags: [:error_view_case]
Excluding tags: [:test]
...
Finished in 0.1 seconds
5 tests, 0 failures, 3 excluded
Randomized with seed 125659
注意:ExUnit 会告诉我们它为每次测试运行包含和排除了哪些标签。如果我们回顾上一节关于运行测试的内容,我们会发现为单个测试指定的行号实际上被视为标签。
$ mix test test/hello_web/controllers/error_html_test.exs:11
Including tags: [line: "11"]
Excluding tags: [:test]
.
Finished in 0.2 seconds
2 tests, 0 failures, 1 excluded
Randomized with seed 364723
为 error_view_case
指定值 true
会产生相同的结果。
$ mix test --only error_view_case:true
Including tags: [error_view_case: "true"]
Excluding tags: [:test]
...
Finished in 0.1 seconds
5 tests, 0 failures, 3 excluded
Randomized with seed 833356
但是,为 error_view_case
指定值 false
不会运行任何测试,因为我们的系统中没有标签匹配 error_view_case: false
。
$ mix test --only error_view_case:false
Including tags: [error_view_case: "false"]
Excluding tags: [:test]
Finished in 0.1 seconds
5 tests, 0 failures, 5 excluded
Randomized with seed 622422
The --only option was given to "mix test" but no test executed
我们可以以类似的方式使用 --exclude
标志。这将运行除错误视图案例中的测试之外的所有测试。
$ mix test --exclude error_view_case
Excluding tags: [:error_view_case]
.
Finished in 0.2 seconds
5 tests, 0 failures, 2 excluded
Randomized with seed 682868
为标签指定值对 --exclude
的作用与对 --only
的作用相同。
我们可以标记单个测试以及完整的测试用例。让我们标记错误视图案例中的几个测试,看看它是如何工作的。
defmodule HelloWeb.ErrorHTMLTest do
use HelloWeb.ConnCase, async: true
@moduletag :error_view_case
# Bring render/4 and render_to_string/4 for testing custom views
import Phoenix.Template
@tag individual_test: "yup"
test "renders 404.html" do
assert render_to_string(HelloWeb.ErrorView, "404", "html", []) ==
"Not Found"
end
@tag individual_test: "nope"
test "renders 500.html" do
assert render_to_string(HelloWeb.ErrorView, "500", "html", []) ==
"Internal Server Error"
end
end
如果我们想只运行标记为 individual_test
的测试,无论其值如何,这都可以。
$ mix test --only individual_test
Including tags: [:individual_test]
Excluding tags: [:test]
..
Finished in 0.1 seconds
5 tests, 0 failures, 3 excluded
Randomized with seed 813729
我们还可以指定一个值并只运行具有该值的测试。
$ mix test --only individual_test:yup
Including tags: [individual_test: "yup"]
Excluding tags: [:test]
.
Finished in 0.1 seconds
5 tests, 0 failures, 4 excluded
Randomized with seed 770938
类似地,我们可以运行除标记为给定值的测试之外的所有测试。
$ mix test --exclude individual_test:nope
Excluding tags: [individual_test: "nope"]
...
Finished in 0.2 seconds
5 tests, 0 failures, 1 excluded
Randomized with seed 539324
我们可以更具体一些,排除错误视图案例中的所有测试,除了标记为 individual_test
且值为 “yup” 的测试。
$ mix test --exclude error_view_case --include individual_test:yup
Including tags: [individual_test: "yup"]
Excluding tags: [:error_view_case]
..
Finished in 0.2 seconds
5 tests, 0 failures, 1 excluded
Randomized with seed 61472
最后,我们可以配置 ExUnit 默认排除标签。默认的 ExUnit 配置在 test/test_helper.exs
文件中完成
ExUnit.start(exclude: [error_view_case: true])
Ecto.Adapters.SQL.Sandbox.mode(Hello.Repo, :manual)
现在,当我们运行 mix test
时,它只运行 page_controller_test.exs
和 error_json_test.exs
中的规范。
$ mix test
Excluding tags: [error_view_case: true]
.
Finished in 0.2 seconds
5 tests, 0 failures, 2 excluded
Randomized with seed 186055
我们可以使用 --include
标志覆盖此行为,告诉 mix test
包含标记为 error_view_case
的测试。
$ mix test --include error_view_case
Including tags: [:error_view_case]
Excluding tags: [error_view_case: true]
....
Finished in 0.2 seconds
5 tests, 0 failures
Randomized with seed 748424
这种技术对于控制运行时间很长的测试非常有用,你可能只想在 CI 或特定情况下运行它们。
随机化
以随机顺序运行测试是一种确保测试真正隔离的好方法。如果我们注意到某个测试出现零星失败,可能是因为之前的测试以未清理的方式改变了系统状态,从而影响了后续的测试。这些失败可能只在以特定顺序运行测试时才会出现。
ExUnit 默认情况下会随机化测试运行的顺序,使用一个整数来播种随机化。如果我们注意到特定的随机种子触发了我们的间歇性失败,我们可以使用相同的种子重新运行测试,以可靠地重新创建该测试序列,以帮助我们找出问题所在。
$ mix test --seed 401472
....
Finished in 0.2 seconds
5 tests, 0 failures
Randomized with seed 401472
并发和分区
正如我们所见,ExUnit 允许开发人员并发运行测试。这使开发人员能够利用机器中的所有能力,尽可能快地运行他们的测试套件。结合 Phoenix 的性能,大多数测试套件的编译和运行时间比其他框架要短得多。
虽然开发人员在开发过程中通常可以使用功能强大的机器,但在持续集成服务器中可能并非总是如此。为此,ExUnit 还支持测试环境中的开箱即用的测试分区。如果你打开你的 config/test.exs
文件,你会发现数据库名称被设置为
database: "hello_test#{System.get_env("MIX_TEST_PARTITION")}",
默认情况下,MIX_TEST_PARTITION
环境变量没有值,因此它没有效果。但在你的 CI 服务器中,你可以使用四个不同的命令在不同的机器上拆分你的测试套件
$ MIX_TEST_PARTITION=1 mix test --partitions 4
$ MIX_TEST_PARTITION=2 mix test --partitions 4
$ MIX_TEST_PARTITION=3 mix test --partitions 4
$ MIX_TEST_PARTITION=4 mix test --partitions 4
这就是你需要做的,ExUnit 和 Phoenix 会处理所有其余事项,包括使用不同的名称为每个不同的分区设置数据库。
更进一步
虽然 ExUnit 是一个简单的测试框架,但它通过 mix test
命令提供了一个非常灵活且健壮的测试运行器。我们建议你运行 mix help test
或 在线阅读文档
我们已经看到了 Phoenix 在新生成应用程序中为我们提供的功能。此外,每当你生成一个新的资源时,Phoenix 也会为该资源生成所有适当的测试。例如,你可以通过在应用程序根目录运行以下命令来创建具有模式、上下文、控制器和视图的完整脚手架
$ mix phx.gen.html Blog Post posts title body:text
* creating lib/hello_web/controllers/post_controller.ex
* creating lib/hello_web/controllers/post_html/edit.html.heex
* creating lib/hello_web/controllers/post_html/index.html.heex
* creating lib/hello_web/controllers/post_html/new.html.heex
* creating lib/hello_web/controllers/post_html/show.html.heex
* creating lib/hello_web/controllers/post_html/post_form.html.heex
* creating lib/hello_web/controllers/post_html.ex
* creating test/hello_web/controllers/post_controller_test.exs
* creating lib/hello/blog/post.ex
* creating priv/repo/migrations/20211001233016_create_posts.exs
* creating lib/hello/blog.ex
* injecting lib/hello/blog.ex
* creating test/hello/blog_test.exs
* injecting test/hello/blog_test.exs
* creating test/support/fixtures/blog_fixtures.ex
* injecting test/support/fixtures/blog_fixtures.ex
Add the resource to your browser scope in lib/demo_web/router.ex:
resources "/posts", PostController
Remember to update your repository by running migrations:
$ mix ecto.migrate
现在让我们按照说明将新资源路由添加到我们的 lib/hello_web/router.ex
文件中,并运行迁移。
当我们再次运行 mix test
时,我们发现现在有 21 个测试!
$ mix test
................
Finished in 0.1 seconds
21 tests, 0 failures
Randomized with seed 537537
现在,我们处于一个很好的位置,可以过渡到其他测试指南,在这些指南中,我们将更详细地检查这些测试,并添加一些我们自己的测试。