Functional 的好處之一就是,很容易寫測試。大部分的函式都是所謂的純函式,只要給定參數,就會有固定的輸出。不像物件導向有 self
, this
之類的東西,在函數式語言中,我們說這種好處是沒有 side effect,只有輸入會影響輸出跟在哪時候在哪裡執行都沒有關係。
我們的 Context 就是普通的 Elixir module,所以測試也是普通的 Elixir 測試。
在 Elixir 裡本身就內建了 ExUnit
這個測試框架,所以不需要額外的套件。
前面在專案結構的時候有提到, test
資料夾裡面的檔案會對應到 lib
資料夾,這次我們要幫 lib/gratitude/notes.ex
寫測試,我們先建立一個 test/gratitude/notes_test.exs
檔案。
defmodule Gratitude.NotesTest do
use Gratitude.DataCase, async: true
test "1 + 1 應該要等於 2" do
result = 1 + 1
assert result == 2
end
end
當我們在這個測試 Module 加上了 use ExUnit.Case
之後,這個就可以使用 test
這個 macro 來定義測試,後面街上要測試的描述,最後在 do
end
之間寫這項測試。
(暫時不用管 macro ,暫時先把 test 當成是一個函式即可)
在測試裡面 我們使用 assert
函數來確認結果,如果內容是 trucy 的,就會通過測試,如果是 falsy 的,就會失敗。
(在 Elixir 裡面,只有 false
和 nil
是 falsy,其他都是 trucy,包含空的 list, map, string, 0, 等等)
我們可以在終端機執行 mix test
來執行測試,
......
Finished in 0.06 seconds (0.02s async, 0.04s sync)
6 tests, 0 failures
這邊可以看到所有的測試都通過了 (1個我們剛剛寫的,與其他內建的測試)
如果我們要測試目前的檔案就好,甚至是目前檔案行數的這個測試
可以在後面加上檔案位置與行數
mix test test/gratitude/notes_test.exs:5
我們開始幫 Context 裡的函式寫測試,有的時候這個 Context 不只有操作一個 schema 的資料,如果有多個 schema 或是有明顯的邏輯可以區別的話,我們可以使用 describe 將測試分類。
加上 describe 與裡面的第一個測試
defmodule Gratitude.NotesTest do
use ExUnit.Case, async: true
describe "notes" do
test "list_notes/0 回傳所有的 note" do
{:ok, note} = Notes.create_note(%{content: "合格的內容"})
assert Notes.list_notes() == [note]
end
end
end
在這邊我們使用 create_note 來建立一個 note
因為測試的時候資料庫是沙盒模式,所以每次的測試都是獨立的,因此建立一個 note 後他會是資料庫裡面唯一的一個 note,因此我們可以直接使用 ==
檢查 list_notes() 回傳的結果是否是只有這一個 note 的 list。
再加上其他的測試
test "get_note/1 使用 id 找到相對應的 note" do
{:ok, note} = Notes.create_note(%{content: "合格的感激筆記內容"})
assert Notes.get_note(note.id) == note
end
test "create_note/1 建立新的 note" do
valid_attrs = %{content: "合格的感激筆記內容"}
assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs)
assert note.content == "合格的感激筆記內容"
end
在這邊我們使用了 pattern maching 來讓 assert 檢查回傳回來的結構是否正確。
如果回傳的是 {:error, ...}
導致 pattern maching 失敗,我們的測試就會失敗。
test "create_note/1 回傳錯誤 changeset,如果輸入格式不符" do
assert {:error, %Ecto.Changeset{}} = Notes.create_note(%{content: nil})
end
另外這個函式會依照我們傳入的參數而有兩種結果,我們這邊就可以寫兩個把情況都測試到。
test "update_note/2 更新 note 內容" do
{:ok, note} = Notes.create_note(%{content: "合格的感激筆記內容"})
update_attrs = %{content: "被修改的內容"}
assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs)
assert note.content == "被修改的內容"
end
test "update_note/2 使用不合格的輸入會回傳錯誤 changeset" do
{:ok, note} = Notes.create_note(%{content: "合格的感激筆記內容"})
assert {:error, %Ecto.Changeset{}} = Notes.update_note(note, %{content: nil})
assert note == Notes.get_note(note.id)
end
更新的測試與新增相當類似,只差有先建立一個原本的 note,
最後是刪除,除了用 pattern maching 刪除的回傳值之外,我們還使用 get_note 來確認該 note 是不是真的被刪除了。
test "delete_note/1 刪除 note" do
{:ok, note} = Notes.create_note(%{content: "合格的感激筆記內容"})
assert {:ok, %Note{}} = Notes.delete_note(note)
assert nil == Notes.get_note(note.id)
end
在下一次的測試我們可以再來改進這邊的內容,有一些功能與套件可以讓我們更加方便,不過我們可以先來寫畫面了!