檢視原始碼 測試情境

需求:本指南假設你已閱讀入門指南並有運作中的 Phoenix 應用程式 運作中

需求:本指南假設你已閱讀測試簡介

需求:本指南假設你已閱讀情境指南。

在測試簡介指南的結尾,我們使用以下指令為貼文產生 HTML 資源:

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

這讓我們免費取得許多模組,包括 Blog 情境和貼文結構,及它們各自的測試檔案。正如我們在情境指南中所學,Blog 情境只是一個模組,其功能鎖定我們的業務領域的特定區域,而貼文結構則對應資料庫中的特定表格。

在本指南中,我們將探討為情境和結構產生的測試。再做其他事之前,我們可以執行 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" 方塊。在 ExUnit 中,describe 方塊可以用來將功能相似的測試分組。我們將所有與貼文相關的測試分組在一起的原因,是因為 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 會將所有別名和匯入引入我們的測試。 DataCaseset up 塊與 ConnCase 相似。正如我們所見,大部分的 setup 區塊都是用來設定 SQL Sandbox。

SQL Sandbox 讓我們可以將測試寫入資料庫,而不會影響其他測試。簡而言之,我們會在每個測試開始時在資料庫中開始一個交易。完成測試後,我們會自動回滾交易,這樣等於抹除了在測試中新增的所有資料。

更棒的是,即使 SQL Sandbox 同時與資料庫溝通,它也可以讓多個同時執行。這個功能是提供給 PostgreSQL 資料庫,而且在使用時可以透過新增 async: true 旗標,進一步加快你的環境和 controller 測試。

use Hello.DataCase, async: true

在使用沙盒執行非同步測試時,你需要留意一些注意事項,請參閱 Ecto.Adapters.SQL.Sandbox 以取得更多資訊。

最後,在 DataCase 模組的結尾,我們可以找到一個名為 errors_on 的函式,其中有一些示範說明如何使用它。這個函式是為了要測試我們想新增到範本中的任何驗證而使用的。讓我們透過新增自己的驗證再進行測試來試用這個函式。

測試範本

當我們產生 HTML 文章資源時,Phoenix 會同時產生 Blog 環境和 Post 範本。它會為環境產生測試檔案,但不會為範本產生測試檔案。然而,這並不表示我們不需要測試範本,這只是表示我們到目前為止不需要測試範本。

你可能會想:我們什麼時候直接測試環境,什麼時候直接測試範本?這個問題的解答,跟我們什麼時候將程式碼新增到環境,什麼時候新增到範本的問題解答是一樣的。

通則為將所有無副作用的程式碼放在 schema 裡。換句話說,如果你只是在處理資料結構、schema 和 changeset,那就把它放在 schema 裡。內容通常會有建立和更新 schemas 的程式碼,然後寫入資料庫或 API。

我們將為 schema 模組新增其他驗證,這是一個編寫特定於 schema 的測試的好時機。開啟 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

就這樣。隨著我們的業務領域成長,我們有了定義良好的地方來測試我們的 context 和 schema。