檢視原始碼 測試簡介
測試已成為軟體開發流程中不可或缺的一部分,而輕鬆撰寫有意義的測試的能力,對任何現代網路架構來說都是不可或缺的功能。Phoenix 認真看待這一點,提供支援檔案,讓架構的所有主要元件都能輕鬆測試。它還會在任何已產生模組旁產生範例的測試模組,協助我們開始進行。
Elixir 內建一個稱為ExUnit的測試架構。ExUnit 致力於清晰明確,盡量減少使用魔力。Phoenix 使用 ExUnit 進行所有測試工作,我們也會在這裡使用它。
執行測試
當 Phoenix 為我們產生一個網路應用程式時,它也會包含測試。要執行它們,只要輸入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
如果您要在 Phoenix 外部撰寫 Elixir 程式庫,您可以用 use ExUnit.Case
取代 use HelloWeb.ConnCase
。不過,Phoenix 已經內建許多控制器測試功能,而且 HelloWeb.ConnCase
建立在 ExUnit.Case
的基礎上,提供了這些功能。我們將很快探索 HelloWeb.ConnCase
模組。
然後以 test/3
巨集來定義每個測試。 test/3
巨集會接收三個引數:測試名稱、與我們樣式比對的測試內容以及測試內容。在這個測試中,我們以 get/2
巨集對路徑「/」發出「GET」HTTP 要求,以存取應用程式之根頁面。然後我們斷言已呈現的頁面包含字串「從雛形到生產的安心」。
在 Elixir 中撰寫測試時,我們會使用斷言來檢查結果是否為真。在我們的案例中, assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
會執行兩個動作
- 它斷言
conn
已呈現回應 - 它斷言回應具有狀態碼 200(在 HTTP 術語中表示為 OK)
- 它斷言回應的類型為 HTML
- 它斷言
html_response(conn, 200)
(為 HTML 回應)的結果中包含字串「從雛形到生產的安心」
然而,我們在 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
區塊會在測試之前被呼叫。大多數的 setup
區塊都在建立 SQL Sandbox,我們稍後會討論。在 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
檔案中,並執行 migrations。
當我們再次執行 mix test
,就會看到我們現在已經有二十一個測試!
$ mix test
................
Finished in 0.1 seconds
21 tests, 0 failures
Randomized with seed 537537
至此,我們已經處於一個可以順利轉移到「其餘測試指南」的階段,我們將在這些指南中更詳細地檢視這些測試,並新增我們自己的測試。