查看源代码 测试上下文

要求: 本指南假定您已阅读过入门指南并成功启动了 Phoenix 应用程序 运行.

要求: 本指南假定您已阅读过测试入门指南.

要求: 本指南假定您已阅读过上下文指南.

在测试入门指南的最后,我们使用以下命令生成了一个用于帖子的 HTML 资源

$ mix phx.gen.html Blog Post posts title body:text

这为我们免费提供了一些模块,包括一个 Blog 上下文和一个 Post 模式,以及它们各自的测试文件。正如我们在上下文指南中所学到的,Blog 上下文只是一个模块,它包含了我们业务领域特定区域的功能,而 Post 模式映射到我们数据库中的特定表。

在本指南中,我们将探索为我们的上下文和模式生成的测试。在我们进行任何操作之前,让我们运行mix test,以确保我们的测试套件能够干净地运行。

$ mix test
................

Finished in 0.6 seconds
21 tests, 0 failures

Randomized with seed 638414

很棒,我们有 21 个测试,并且它们都通过了!

测试帖子

如果您打开 test/hello/blog_test.exs,您将看到一个包含以下内容的文件

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

    ...

在文件开头,我们导入了 Hello.DataCase,它与 HelloWeb.ConnCase 相似,我们很快就会看到。虽然 HelloWeb.ConnCase 设置了用于处理连接的辅助函数,这在测试控制器和视图时非常有用,但 Hello.DataCase 提供了用于处理上下文和模式的功能。

接下来,我们定义了一个别名,因此我们可以简单地将 Hello.Blog 称为 Blog

然后我们开始一个 describe "posts" 块。一个 describe 块是 ExUnit 中的一项功能,它允许我们对相似的测试进行分组。我们之所以将所有与帖子相关的测试分组在一起,是因为 Phoenix 中的上下文能够将多个模式分组在一起。例如,如果我们运行以下命令

$ mix phx.gen.html Blog Comment comments post_id:references:posts body:text

我们将获得 Hello.Blog 上下文中的许多新函数,以及我们的测试文件中全新的 describe "comments" 块。

为我们的上下文定义的测试非常直截了当。它们调用我们上下文中的函数并断言其结果。正如您所见,其中一些测试甚至在数据库中创建了条目

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"
end

此时,您可能想知道:Phoenix 如何确保在一个测试中创建的数据不会影响其他测试?我们很高兴您能提出这个问题。为了回答这个问题,让我们来谈谈 DataCase

DataCase

如果您打开 test/support/data_case.ex,您将找到以下内容

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

Hello.DataCase 是另一个ExUnit.CaseTemplate。在 using 块中,我们可以看到 DataCase 带到我们的测试中的所有别名和导入。 DataCasesetup 部分与 ConnCase 的非常相似。正如我们所见,大部分 setup 块都围绕着设置 SQL 沙盒。

SQL 沙盒正是允许我们的测试写入数据库而不会影响任何其他测试的原因。简而言之,在每个测试开始时,我们在数据库中启动一个事务。当测试结束时,我们自动回滚事务,有效地删除了测试中创建的所有数据。

此外,SQL 沙盒允许多个测试同时运行,即使它们与数据库通信。此功能适用于 PostgreSQL 数据库,并且可以通过在使用它们时添加 async: true 标志来进一步加速您的上下文和控制器测试

use Hello.DataCase, async: true

在使用沙盒运行异步测试时,您需要牢记一些注意事项,因此请参阅 Ecto.Adapters.SQL.Sandbox 了解更多信息。

最后,在 DataCase 模块的末尾,我们可以找到一个名为 errors_on 的函数,其中包含一些关于如何使用它的示例。此函数用于测试我们可能想要添加到模式中的任何验证。让我们尝试一下,添加我们自己的验证,然后测试它们。

测试模式

当我们生成 HTML Post 资源时,Phoenix 生成了一个 Blog 上下文和一个 Post 模式。它为上下文生成了一个测试文件,但没有为模式生成测试文件。但是,这并不意味着我们不需要测试模式,只是意味着到目前为止我们还没有测试模式。

您可能想知道:我们什么时候直接测试上下文,什么时候直接测试模式?这个问题的答案与我们什么时候将代码添加到上下文,什么时候将代码添加到模式中的答案相同。

一般准则是将所有无副作用代码保存在模式中。换句话说,如果您只是处理数据结构、模式和变更集,请将其放在模式中。上下文通常包含创建和更新模式的代码,然后将其写入数据库或 API。

我们将向模式模块添加额外的验证,因此这是一个编写一些模式特定测试的好机会。打开 lib/hello/blog/post.ex 并将以下验证添加到 def changeset

def changeset(post, attrs) do
  post
  |> cast(attrs, [:title, :body])
  |> validate_required([:title, :body])
  |> validate_length(:title, min: 2)
end

新的验证说标题必须至少包含 2 个字符。让我们为此编写一个测试。在 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

就是这样。随着我们的业务领域的增长,我们拥有明确定义的位置来测试我们的上下文和模式。