測試跟文件是許多 developer 最不想面對,卻也是非常重要的部份。 Elixir 在相關工具的整合上非常用心,讓大家能輕鬆愉快養成寫文件與測試的好習慣。由於這些工具大多都是 Elixir 標準函式庫,或是官方維護的函式庫,所以不限於 Phoenix,任何 Elixir 專案都可以使用,本篇要來介紹這些工具的用法。
先從最基本的開始:Elixir 內建的測試框架叫做 ExUnit,只要建立一個 .exs
* 檔案,並在模組宣告的下一行加入 use ExUnit.Case
,就可以使用此測試框架提供的功能。依慣例大部份的測試檔案都會放在 專案名稱/test
下, 這裡是一個 Elixir 專案的測試檔範例:
defmodule ExampleTest do
use ExUnit.Case
test "the truth" do
assert 1 + 1 == 2
end
end
用 test
標註測試名稱,在 do...end
區塊裡放測試的內容。在測試的本體部份,則用 assert/1
來驗證。另外你也可以用 describe/2
來將測試分組。
而在 ExUnit.Assertions 文件下,可以找到驗證各種情況的函式,例如確保這個執行會有 exception (assert_raise/2
)、確認兩個值之間的差小於特定範圍 (assert_in_delta/3
) 等等。
Note *: 其實也可以用 .ex
檔案,但一般來說不會想要編譯並保存測試檔的 beam binary,所以慣例上是用 .exs
。
在專案下,輸入 mix test
就可以執行專案下的所有測試案例:
$ mix test
....................
Finished in 0.2 seconds
20 tests, 0 failures
Randomized with seed 693717
打開我們新建專案中的 test
資料夾,會發現測試檔也依專案結構放在不同的資料夾內。在 test/hello_phx/blog
裡的 blog_test.exs
前幾行長這樣:
# test/hello_phx/blog/blog_test.exs
defmodule HelloPhx.BlogTest do
use HelloPhx.DataCase
# ...
注意第二行並非我們之前所說的 use ExUnit.Case
,而是掛了我們專案命名空間的 HelloPhx.DataCase
。要了解這是什麼,得要看看 test/support/data_case.ex
這張檔案:
# test/support/data_case.ex
defmodule HelloPhx.DataCase do
use ExUnit.CaseTemplate
using do
quote do
alias HelloPhx.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import HelloPhx.DataCase
end
end
setup tags do
# ...
end
def errors_on(changeset) do
# ...
end
end
從檔案的最上方可以查到這是使用了 ExUnit 的 CaseTemplate。這個功能可以讓你 (或框架) 定義不同的測試環境及共用測試函式。利用了 macro 的機制,上方 quote do...end
區塊裡的程式碼,會被嵌入到每一張 use
了這個模組的檔案裡。
而 Phoenix 幫你預設了三種情況:
上面的那些都蠻無聊的,我知道。現在要來些有趣的了。許多人一想到測試程式,直覺浮現的大概就是這樣的程式碼,也就是 Elixir 自動幫你生成的那種:
test "length/1 calculates the length of a list" do
assert length([]) == 0
assert length([:one]) == 1
assert length([1, 2, 3]) == 3
end
這種測試方式有個名字,叫做 example-based approach test。意思是我們測試的資料,是靠寫測試的人自己想出一些正常範例、及一些邊界範例,一個個把這些範例的測試結果寫下來。但這樣的缺點是有時你會漏掉邊界範例,有時實作改變時,當初想的範例還是正常的,但邊界條件已經改變了。
*啊要不然還能怎樣?*你可能會這樣想。
記得我們之前講過的惰性求值,Lazy evaluation 嗎?那個章節說到你可以擁有無限大的起始值。那麼放在測試裡,我們可以這樣想:你可以擁有無限大的測試資料,例如說:
對於任兩個正整數,
+/2
的結果必然比兩個數字都大。
這種測試做法稱之為 property-based testing,基於特性的測試。你給定測試資料的特性,測試時會自動生出符合該特性的多筆資料來執行測試。這種測試在 Haskell 及 Erlang 已行之有年,而 Elixir 基於原本 Dave Thomas 所寫的 pollution 實作了一個包含資料生成及測試語法的 StreamData,並預計在1.6 或 1.7 版正式併入標準函式庫。
而 property-based testing 的語法將會長這樣:
defmodule MyPropertyTest do
use ExUnit.Case, async: true
use ExUnitProperties
property "sum of positive integer is greater than both integers" do
check all a <- integer(),
b <- integer(),
a > 0 and b > 0,
sum = a + b do
assert sum > a
assert sum > b
end
end
end
Elixir 的預設文件格式是 Markdown,只要在程式裡用 @moduledoc
或是 @doc
就可以了,像這樣:
defmodule MyModule do
@moduledoc """
模組文件
"""
@doc """
函式文件
"""
def my_function do
end
而要生成文件,只要在 mix.exs
裡加入 ex_doc
這個 dependency:
def deps do
[{:ex_doc, "~> 0.16", only: :dev, runtime: false}]
end
接著用 mix deps.get
安裝。再輸入 mix docs
就會自動生成文件了。
所以你看到的 Elixir 函式庫文件都長同一個樣子,原因就是大家都用內建的工具來生成。好處就是查起來很順手,功能也非常齊全,不需要每次都重新適應。
在前面的說明藏了一個線索,說:「依慣例大部份的測試檔案都會放在 專案名稱/test
下。」,也就是說,並非所有的測試都會寫在那裡面。這個好東西叫做 doctest。因為 Elixir 的文件格式是 Markdown,而大家都知道 Markdown 可以內嵌程式碼,所以你可以在文件裡這樣寫:
defmodule Num do
@doc """
Demonstrate doctest feature
## Example
iex> Num.is_even?(1)
true
"""
def is_even?(num) do
rem(num, 2) == 0
end
end
就可以生成這樣的文件 (注意程式碼的部份):
我知道,Num.is_even?(1)
應該是錯的,我是故意的。
用 mix test
跑測試的時候,你會看到這樣的結果:
沒錯,你寫在文件裡的 iex>
的程式碼會被執行,並用下一行的值做為測試比對的結果。之所以可以做到這件事,你可以打開 test/專案名稱.exs
測試檔來看,會發現這麼一行:
doctest 專案名稱
代表在執行測試時,會去翻實作的程式碼文件,找到裡面的程式範例來執行。
程式、文件跟測試都乾淨的寫在同一張檔案裡,是所謂三位一體。
接下來會花一些篇幅來解釋 Phoenix 中的重要功能: Channel。
Happy hacking! 下次見。