erlang 單元測試
learn you some erlang for great good
歐盟國家委員會
測試的需要
我們編寫的軟件隨著時間的推移變的越來越大,越來越復(fù)雜.
當(dāng)發(fā)生這種情況時,啟動 Erlang shell, 輸入內(nèi)容,查看結(jié)果以及確保代碼更改后,能夠正常工作.
隨著時間的推移,每個人都可以更加簡單的運(yùn)行之前準(zhǔn)備好的測試代碼,而不是一直按照清單去手工檢查軟件里的所有測試案例.
您也可能是測試驅(qū)動開發(fā)的粉絲,因此也會發(fā)現(xiàn)測試很有用.
如果你還記得我們編寫 RPN 計算器的章節(jié),我們有一些手動編寫的測試方法.
它們只是一組 Result = Expression 形式的模式匹配,如果出現(xiàn)問題就會崩潰,否則會成功.
這適用于您自己編寫的簡單代碼,但是當(dāng)我們進(jìn)行更嚴(yán)格的測試時,我們肯定會想要更好的東西,比如:框架.
對于單元測試,我們傾向于堅持 EUnit, 我們在本章中看到的.
對于集成測試, EUnit 和 Common Test 都可以完成這項工作.
事實(shí)上, Common Test 可以完成單元測試到系統(tǒng)測試,實(shí)質(zhì)是外部軟件測試的所有工作,而不是用 erlang 編寫的.
現(xiàn)在我們將使用 EUnit, 因為它產(chǎn)生好的結(jié)果是那么簡單.
單元測試,什么是單元測試
EUnit,最簡單的形式,只是模塊中以 _test 結(jié)尾的自動運(yùn)行的方法,就認(rèn)為他們是單元測試.
如果你去發(fā)掘我上面提到的 RPN 計算器,你會發(fā)現(xiàn)以下代碼:
rpn_test() ->
5 = rpn("2 3 +"),
87 = rpn("90 3 -"),
-4 = rpn("10 4 3 + 2 * -"),
-2.0 = rpn("10 4 3 + 2 * - 2 /"),
ok = try
rpn("90 34 12 33 55 66 + * - +")
catch
error:{badmatch,[_|_]} -> ok
end,
4037 = rpn("90 34 12 33 55 66 + * - + -"),
8.0 = rpn("2 3 ^"),
true = math:sqrt(2) == rpn("2 0.5 ^"),
true = math:log(2.7) == rpn("2.7 ln"),
true = math:log10(2.7) == rpn("2.7 log10"),
50 = rpn("10 10 10 20 sum"),
10.0 = rpn("10 10 10 20 sum 5 /"),
1000.0 = rpn("10 10 20 0.5 prod"),
ok.
這正是我們編寫的測試函數(shù),以確保計算器工作正常.找到之前的模塊,并嘗試運(yùn)行以下命令.
1> c(calc).
{ok,calc}
2> eunit:test(calc).
Test passed.
ok
調(diào)用eunit:test(Module).
正是我們需要的,是的,我們現(xiàn)在知道了 EUnit, 打開香檳,讓我們進(jìn)入一個不同的章節(jié).
顯然,只做這一點(diǎn)測試框架不會非常有用,而且在技術(shù)程序員的術(shù)語中,他可能被描述為"不太好".
EUnit 不僅僅是自動導(dǎo)出和運(yùn)行以 _test()
結(jié)尾的函數(shù).
例如,您可以將測試移動到另一個模塊,以便您的代碼和測試不會混合在一起.
這意味著,您不能再測試私有函數(shù),但也意味著,如果您針對模塊的接口(導(dǎo)出的函數(shù))開發(fā)所有測試,那么在重構(gòu)代碼時,不需要重寫測試.
讓我們嘗試用兩個簡單的模塊分離測試和代碼.
所以我們有 ops 和 ops_test, 其中第二個包括與第一個相關(guān)的測試.這是 EUnit 可以做的事情.
調(diào)用 eunit:test(Mod)
會自動查找 Mod_tests 并在其中運(yùn)行測試.讓我們稍微改變測試(使其成為 3= ops:add(2,2)) 以查看失敗的樣子
我們可以看到測試失敗的結(jié)果和原因.
我們獲得了有關(guān)通過和失敗的測試數(shù)量的完整報告.雖然輸出很糟糕.
至少和普通的 Erlang 崩潰一樣糟糕:沒有行號,沒有明確的解釋,并沒有確切的匹配什么內(nèi)容 等等.
偶爾們對運(yùn)行測試的測試框架感到無助,但并沒有告訴你太多關(guān)于他們的信息.
出于這個原因, EUnit 引入了一些宏來幫助我們.他們中的每一個都將為我們提供更清晰的報告(包括行號)和更清晰的語義.
他們知道出了什么問題和知道出錯的原因是不同的
?assert(Expression), ?assertNot(Expression)
將測試布爾值.如果除 true 之外的任何值傳入 ?assert ,將會展示錯誤提示.同樣對于 ?assertNot,
對于不正確的值,這個宏相當(dāng)于執(zhí)行了 true =X 或者 false=Y.
?assertEqual(A, B)
在兩個表達(dá)式之間,進(jìn)行嚴(yán)格比較,相當(dāng)于=:=, 如果他們不等,將會提示失敗.這大致相當(dāng)于 true= X=:=Y
.
從 R14B04版本開始,宏 ?assertNorEqual
是與 ?assertEqual
相反的操作
?assertMatch(Pattern, Expression)
這允許我們以類似于 Pattern = Expression
的形式進(jìn)行匹配,而不需要綁定變量.
這意味著我可以執(zhí)行類似的操作: ?aseertMatch({X,X},some_function())
,并斷言我收到一個元素相同的元組.
而且,我以后可以做 ?assertMatch(X,Y)
, 并且 X不用綁定
這就是說,不用像 Pattern= Expression,
我們更接近于 (fun(Pattern) -> true; (_)-> erlang:error(nomatch) end)(Expressson)
:
模式中的變量頭,永遠(yuǎn)不會收到多個斷言的綁定.
?assertNotMatch
在 EUnitR14B04版本中加入
?assertError(Pattern, Expression)
告訴 EUnit 表達(dá)式應(yīng)該導(dǎo)致錯誤, 例如: ?assertError(badarith,1/0)
將會是一次成功的測試
?assertThrow(Pattern, Expression)
與 ?assertError
完全相同,但是使用 exit(Pattern)
(并不是 exit/2)并不是erlang:error(Pattern)
?assertException(Class, Pattern, Expression)
先前三個宏一樣的形式,例如:?assertException(error,Pattern,Expression)
,與?assertError(Pattern,Expression)
相同,從 R14B04版本開始,還有可以用于 ?assertNotException/3
的宏
使用這些宏,我們可以再我們的模塊中編寫更好的測試
-module(ops_tests).
-include_lib("eunit/include/eunit.hrl").
add_test() ->
4 = ops:add(2,2).
new_add_test() ->
?assertEqual(4, ops:add(2,2)),
?assertEqual(3, ops:add(1,2)),
?assert(is_number(ops:add(1,2))),
?assertEqual(3, ops:add(1,1)),
?assertError(badarith, 1/0).
看看錯誤報告有多好,我們知道 ops_tests第11行的 assertEqual 失敗了.
當(dāng)我們調(diào)用 ops:add(1,1)
,我們認(rèn)為會得到3作為值,但是我們得到2
當(dāng)然,您必須將這些值當(dāng)做 erlang 術(shù)語來讀,但至少他們在那里
然而,令人討厭的是,即使我們有5個斷言,但是只有一個失敗,但整個測試仍然被視為失敗
如果知道某些斷言失敗,而沒有表現(xiàn)的所有其他斷言也失敗,那就更好了
我們的測試相當(dāng)于在學(xué)形ナ考試,一旦你犯錯,你就會失敗并被拋棄.
然后你就像是一只死狗了,而且你只有一個可怕的一天了
測試生成器
由于這種對靈活性的共同需求, EUnit 支持成為測試生成器的東西.測試生成器非常簡單,可以用巧妙的方式在稍后運(yùn)行的函數(shù)中包含斷言.
我們將使用以 test() 結(jié)尾的函數(shù)和assertSomething 形式的宏,而不是以 _test()結(jié)尾的函數(shù)和?assertSomething
形式的宏
這些都是微小的變化,但他們使事情變得更加強(qiáng)大.以下兩個測試將是等效的.
function_test() -> ?assert(A == B).
function_test_() -> ?_assert(A == B).
這里, funtion_test_()
被稱為測試生成器函數(shù),而 ?assert(A==B)
被稱作是測試生成器
他被稱為這樣,因為謎底是,?_assert(A==B)
的底層是現(xiàn)實(shí) fun()->?_assert(A==B) end.
也就是說生成測試的函數(shù)
與常規(guī)斷言相比,測試生成器的優(yōu)勢在于他們是 fun 函數(shù),這意味著,可以再不執(zhí)行的情況下操縱他們.
事實(shí)上,我們可以擁有以下形式的測試集
my_test_() ->
[?_assert(A),
[
?_assert(B), ?_assert(C), [?_assert(D)]
],
[[?_assert(E)]]
].
測試集可以試測試生成器的深層嵌套列表.我們擁有可以返回測試函數(shù)的函數(shù),我們將以下內(nèi)容添加到 ops_test:
add_test_() ->
[test_them_types(),
test_them_values(),
?_assertError(badarith, 1/0)].
test_them_types() ->
?_assert(is_number(ops:add(1,2))).
test_them_values() ->
[?_assertEqual(4, ops:add(2,2)),
?_assertEqual(3, ops:add(1,2)),
?_assertEqual(3, ops:add(1,1))].
因為只有 add_test_()
在_ test_
中結(jié)束,所以兩個函數(shù) test_them_Something()
不會被視為測試.實(shí)際上,他們只會被 add_test_
調(diào)用以生成測試.
8> c(ops_tests).
./ops_tests.erl:12: Warning: this expression will fail with a 'badarith' exception
{ok,ops_tests}
9> eunit:test(ops).
ops_tests: new_add_test...*failed*
::error:{assertEqual_failed,[{module,ops_tests},
{line,11},
{expression,"ops : add ( 1 , 1 )"},
{expected,3},
{value,2}]}
in function ops_tests:'-new_add_test/0-fun-3-'/1
in call from ops_tests:new_add_test/0
=======================================================
Failed: 1. Skipped: 0. Passed: 1.
error
所以我們?nèi)匀坏玫筋A(yù)期的失敗,現(xiàn)在你看到我們從2個測試跳到了7個,測試生成器的魔力
如果我們只想測試套件的某些部分,也許只是 add_test_/0
怎么辦,那么 EUnit 有一些技巧
3> eunit:test({generator, fun ops_tests:add_test_/0}).
ops_tests:25: test_them_values...*failed*
::error:{assertEqual_failed,[{module,ops_tests},
{line,25},
{expression,"ops : add ( 1 , 1 )"},
{expected,3},
{value,2}]}
in function ops_tests:'-test_them_values/0-fun-4-'/1
=======================================================
Failed: 1. Skipped: 0. Passed: 4.
error
請注意,這僅適用于測試生成器功能,我們在這里的 {generator,Fun}
就是 EUnit 的用法所謂的測試表示.我們還有一些其他表示
-
{module, Mod}
: 運(yùn)行 module 中的所有測試 -
{dir, Path}
: 運(yùn)行 path 中找到的所有模塊中的測試 -
{file, Path}
: 運(yùn)行在單個編譯模塊中找到的所有測試 -
{generator, Fun}
: 運(yùn)行單個生成器作為測試,如上所示 -
{application, AppName}
: 運(yùn)行 AppName 的. app 文件中提到的所有模塊的所有測試方法
這些不同的測試表示,可以輕松地為整個應(yīng)用程序甚至版本運(yùn)行測試套件
裝置, 測試套件
test fixtur 名詞解釋
- 一個test fixture 表示執(zhí)行一個或多個測試前的準(zhǔn)備工作让簿,以及執(zhí)行完成后清理工作赫舒。
- 例如:創(chuàng)建臨時或代理數(shù)據(jù)庫或目錄蛋逾,或者是一個啟動服務(wù)器進(jìn)程淆游。
- 也可以形象的理解為夾心餅干中的外面兩層
- 成為測試夾具, 測試裝置,腳手架等,實(shí)在找不到合適的翻譯,姑且暫用本身 Fixtures
僅通過使用斷言和測試生成器來測試整個應(yīng)用程序,仍然非常困難.
這就是添加固定裝置的原因, Fixtures 雖然不是讓你的測試運(yùn)行到應(yīng)用程序級別的全能解決方案,但允許你圍繞測試建一個特定的腳手架.
所討論的腳手架是一種通用結(jié)構(gòu),允許我們?yōu)槊總€測試定義設(shè)置和拆卸功能.
這些函數(shù)將允許您構(gòu)建每個測試所需要的狀態(tài)和環(huán)境,此外,腳手架將允許您指定如何運(yùn)行測試,您想在本地,在單獨(dú)的進(jìn)程中運(yùn)行他們嗎.
有幾種類型的腳手架可供選擇,并且各種變化,第一種類型簡稱為設(shè)置腳手架.設(shè)置腳手架可以采用以下多種形式之一.
{setup, Setup, Instantiator}
{setup, Setup, Cleanup, Instantiator}
{setup, Where, Setup, Instantiator}
{setup, Where, Setup, Cleanup, Instantiator}
哎呀,看來我們需要閱讀一些 EUnit 詞匯才能理解這一點(diǎn),(如果您需要閱讀 EUnit 文檔,這將非常有用)
Setup
一個不帶參數(shù)的函數(shù),每個測試都將傳遞 setup 函數(shù)返回的值
cleanup
一種函數(shù),他將 setup 函數(shù)的結(jié)果作為參數(shù),并負(fù)責(zé)清理所需的任何內(nèi)容..如果在 OTP 中終止與 init 相反,則 cleanup 功能和 setup 功能相反
Instantiator 實(shí)例化
她是一個獲取設(shè)置函數(shù)結(jié)果并返回測試集的函數(shù),請記住,測試集可能是深層嵌套的 ?_Macro 斷言列表
where
指定如何運(yùn)行測試, local,spawn,{spawn,node()}
好吧,那么在實(shí)踐中看起來像什么,好吧,讓我想象一下測試,以確保虛擬進(jìn)程注冊表正確處理嘗試注冊相同的進(jìn)程兩次,使用不同的名稱:
double_register_test_() ->
{setup,
fun start/0, % setup function
fun stop/1, % teardown function
fun two_names_one_pid/1}. % instantiator
start() ->
{ok, Pid} = registry:start_link(),
Pid.
stop(Pid) ->
registry:stop(Pid).
two_names_one_pid(Pid) ->
ok = registry:register(Pid, quite_a_unique_name, self()),
Res = registry:register(Pid, my_other_name_is_more_creative, self()),
[?_assertEqual({error, already_named}, Res)].
這個腳手架,首先在 start/0
函數(shù)內(nèi)啟動注冊表服務(wù)器,然后調(diào)用實(shí)例化 two_names_one_pid(REsultFromSetup).
在那個測試中,我唯一要做的就是嘗試兩次注冊當(dāng)前進(jìn)程.
這就是實(shí)例化器工作的地方,第二次注冊的結(jié)果存儲在 Res 變量中,
然后,該函數(shù)將返回包含單個測試的測試集 (?assertEqual({error,already_named},Res))
.
該測試集將由 EUnit 運(yùn)行,然后將調(diào)用 teardown 拆卸方法 stop/1將會被調(diào)用.使用 setup 函數(shù)返回的 pid, 它將能夠關(guān)閉我們事先啟動的注冊表. 美好.
更好的是整個腳手架本身可以放在一個測試裝置中
some_test_() ->
[{setup, fun start/0, fun stop/1, fun some_instantiator1/1},
{setup, fun start/0, fun stop/1, fun some_instantiator2/1},
...
{setup, fun start/0, fun stop/1, fun some_instantiatorN/1}].
這將有效,令人煩惱的是需要始終重復(fù)設(shè)置和拆卸功能,特別是當(dāng)他們始終相同時,這就是第二種類型的腳手架,即 foreach 腳手架進(jìn)入舞臺的地方
{foreach, Where, Setup, Cleanup, [Instantiator]}
{foreach, Setup, Cleanup, [Instantiator]}
{foreach, Where, Setup, [Instantiator]}
{foreach, Setup, [Instantiator]}
foreach腳手架與 setup 腳手架非常相似,區(qū)別在于他需要實(shí)例化表,這是使用 foreach 腳手架編寫 some_test_/0
函數(shù)
some2_test_() ->
{foreach,
fun start/0,
fun stop/1,
[fun some_instantiator1/1,
fun some_instantiator2/1,
...
fun some_instantiatorN/1]}.
那更好,然后 foreach 腳手架將獲取每個實(shí)例化器,并為每個實(shí)例化器運(yùn)行 setup 和 teardown 功能
現(xiàn)在我們知道如何為一個實(shí)例化器設(shè)置一個腳手架,然后為他們中的許多實(shí)例設(shè)置(每個實(shí)例都進(jìn)行setup 和 teardown 函數(shù)調(diào)用).
如果我想要一個setup 函數(shù)調(diào)用,并且一個 teaddown 函數(shù),需要多個實(shí)例化器,該怎么辦
換句話說,如果我有很多實(shí)例化器,但我只想設(shè)置一些狀態(tài)呢. 對此有沒有簡單的辦法,但這可能是一個技巧
some_tricky_test_() ->
{setup,
fun start/0,
fun stop/1,
fun (SetupData) ->
[some_instantiator1(SetupData),
some_instantiator2(SetupData),
...
some_instantiatorN(SetupData)]
end}.
通過使用測試集可以是深層嵌套列表的事實(shí),我們將一對具有匿名函數(shù)實(shí)例化器包裝秤類似于他們的實(shí)例化器
當(dāng)您使用腳手架時,測試還可以對他們應(yīng)該如何運(yùn)行進(jìn)行更精細(xì)的控制,有四種選擇
-
{spawn, TestSet}
- 在主要測試過程之外的單獨(dú)過程中運(yùn)行測試
- 測試過程將等待所有生成的測試完成
-
{timeout, Seconds, TestSet}
- 測試將運(yùn)行數(shù)秒,如果他們花費(fèi)時間超出,他們將被終止,而不會更加輕松
-
{inorder, TestSet}
- 這告訴 EUnit 嚴(yán)格按照返回的順序在測試集中運(yùn)行測試
-
{inparallel, Tests}
- 在可能的情形下,測試將并行運(yùn)行
作為例子, some_tricky_test_/0
測試生成器可以重寫為下面這樣
some_tricky2_test_() ->
{setup,
fun start/0,
fun stop/1,
fun(SetupData) ->
{inparallel,
[some_instantiator1(SetupData),
some_instantiator2(SetupData),
...
some_instantiatorN(SetupData)]}
end}.
這對于腳手架來說,真的是大部,但是現(xiàn)在還有一個我忘了展示的好玩法.
您可以以一種簡潔的方法描述測試,看一下這個
double_register_test_() ->
{"Verifies that the registry doesn't allow a single process to "
"be registered under two names. We assume that each pid has the "
"exclusive right to only one name",
{setup,
fun start/0,
fun stop/1,
fun two_names_one_pid/1}}.
很好,對吧,你可以通過執(zhí)行{Comment,Fixture}
來包裝腳手架,以獲得可讀性,讓我們把它付諸實(shí)踐吧
注冊測試
因為只是看到上面的虛假測試并不是最有趣的事情,并且以為假裝測試不存在的軟件更糟糕
我們將研究我為 regis-1.0.0
進(jìn)程注冊表編寫的測試, 這是 Process Quest
使用的測試
現(xiàn)在, regis 的開發(fā)是以測試驅(qū)動的方式完成的. 希望你不討厭 TDD(測試驅(qū)動開發(fā)),但即使你這樣做,也不應(yīng)該太糟糕,因為事后
我們會看看測試套件
通過這樣做,我們切斷了一些試錯序列,并且我可能已經(jīng)第一次編寫了它,并且由于文本編輯的魔力,我看起來真的很稱職.
regis 應(yīng)用程序由三個過程組成:
- 一個監(jiān)督者,一個主 server, 和一個application 回調(diào)模塊.
- 我們知道 supervisor 只會檢查 server, 并且回調(diào)除了作為兩個模塊之間的接口外,什么都不做
我們可以安全的編寫一個專注于server 本身的測試套件,而不需要任何外部依賴
作為一名優(yōu)秀的 TDD 粉絲,我首先編寫了一分我想要涵蓋的所有功能的列表:
- 尊重類似于 Erlang 默認(rèn)進(jìn)程注冊表的接口
- server 將具有注冊名稱,以便可以在不跟蹤其pid 的情況下聯(lián)系到它
- 可以通過我們的服務(wù)注冊流程,然后可以通過其名稱和它聯(lián)系
- 可以獲得所有已經(jīng)注冊進(jìn)程的列表
- 任何進(jìn)程都沒有注冊的名稱應(yīng)該返回 undefined, 一邊使用它們來崩潰
- 一個進(jìn)程不能有兩個名字
- 兩個進(jìn)程不能共享同一個名字
- 如果已經(jīng)在調(diào)用前取消注冊,可以再次注冊已經(jīng)注冊的進(jìn)程
- 取消注冊進(jìn)程永遠(yuǎn)不會崩潰
- 注冊進(jìn)程崩潰將會取消注冊名稱
這是一個值得尊敬的名單.逐個完成元素并按照我的方式添加案例,我將每個規(guī)范轉(zhuǎn)換為測試.獲得最終的文件是 regis_server_tests.我們使用基本結(jié)構(gòu)寫了一些東西
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
好吧, 我把它給你,當(dāng)模塊是空時看起來很奇怪,但是當(dāng)你填滿它,他會越來越有意義
添加第一個測試后,最初的測試是應(yīng)該可以啟動服務(wù)器并按名稱訪問他,該文件看起來像這樣:
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
{setup,
fun start/0,
fun stop/1,
fun is_registered/1}}.
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
start() ->
{ok, Pid} = regis_server:start_link(),
Pid.
stop(_) ->
regis_server:stop().
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
is_registered(Pid) ->
[?_assert(erlang:is_process_alive(Pid)),
?_assertEqual(Pid, whereis(regis_server))].
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
現(xiàn)在看組織, 已經(jīng)好多了
文件頂部僅包含腳手架和功能的頂級描述
第二部分,包含我們可能需要的裝置和清理功能,我們最后一個包返回測試集的實(shí)例化器
在這種情況下,實(shí)例化器會檢查 regis_server:start_link()
是否生成了一個真正存活的進(jìn)程,
并且它是使用名稱 regis_server 注冊的.如果這是真的,那么這將適用于服務(wù)
如果我們查看文件的當(dāng)前版本,她現(xiàn)在看起來更像是這兩個第一部分.
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
-define(setup(F), {setup, fun start/0, fun stop/1, F}).
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
?setup(fun is_registered/1)}.
register_test_() ->
[{"A process can be registered and contacted",
?setup(fun register_contact/1)},
{"A list of registered processes can be obtained",
?setup(fun registered_list/1)},
{"An undefined name should return 'undefined' to crash calls",
?setup(fun noregister/1)},
{"A process can not have two names",
?setup(fun two_names_one_pid/1)},
{"Two processes cannot share the same name",
?setup(fun two_pids_one_name/1)}].
unregister_test_() ->
[{"A process that was registered can be registered again iff it was "
"unregistered between both calls",
?setup(fun re_un_register/1)},
{"Unregistering never crashes",
?setup(fun unregister_nocrash/1)},
{"A crash unregisters a process",
?setup(fun crash_unregisters/1)}].
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
start() ->
{ok, Pid} = regis_server:start_link(),
Pid.
stop(_) ->
regis_server:stop().
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
%% nothing here yet
不錯,不是嗎. 請注意,在我編寫套件時,我最終看到我從不需要任何其他設(shè)置和拆卸功能,而不是 start/0
和 stop/1
出于這個原因,我添加了 set_up 宏,這使得事情看起來必有腳手架都要完全展開要好一些,
現(xiàn)在很明顯,我將功能列表的每個點(diǎn)都變成了一堆測試
你會注意到我根據(jù)他們是否與啟動和停止有關(guān),注冊進(jìn)程和取消進(jìn)程.
通過閱讀測試生成器的定義,我們可以知道該模塊應(yīng)該做什么.測試生成文檔(盡管他們不應(yīng)取代適當(dāng)?shù)奈臋n)
我們將稍微研究一下測試,看看事情是以某種方式完成.列表 start_stop_test_/0中的第一個測試,只需要注冊服務(wù)
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
?setup(fun is_registered/1)}.
測試本身的實(shí)現(xiàn),放在 is_register/1
函數(shù)中:
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
is_registered(Pid) ->
[?_assert(erlang:is_process_alive(Pid)),
?_assertEqual(Pid, whereis(regis_server))].
如前所述,當(dāng)我們查看測試的第一個版本時,會檢查該過程是否可用.
雖然函數(shù) erlang:is_process_alive(Pid)
對你來說可能不熟悉,但 沒有什么特別之處.
顧名思義,他會檢查進(jìn)程當(dāng)前是否正在運(yùn)行.
我把那個測試放在那里的原因很簡單,一旦我們啟動它很可能服務(wù)器崩潰,或者它從未在第一個時間開始崩潰,我們不希望這樣
第二個測試與能夠注冊流程有關(guān)
{"A process can be registered and contacted",
?setup(fun register_contact/1)}
這是測試代碼的樣子:
register_contact(_) ->
Pid = spawn_link(fun() -> callback(regcontact) end),
timer:sleep(15),
Ref = make_ref(),
WherePid = regis_server:whereis(regcontact),
regis_server:whereis(regcontact) ! {self(), Ref, hi},
Rec = receive
{Ref, hi} -> true
after 2000 -> false
end,
[?_assertEqual(Pid, WherePid),
?_assert(Rec)].
當(dāng)然這不是最優(yōu)雅的測試,他的作用是產(chǎn)生一個過程,它只會注冊自己并回復(fù)我們發(fā)送的一些消息.
這都是在 call_back/1
輔助函數(shù)中完成的,定義如下:
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
callback(Name) ->
ok = regis_server:register(Name, self()),
receive
{From, Ref, Msg} -> From ! {Ref, Msg}
end.
因此該函數(shù)具有模塊寄存器本身,接收消息,并發(fā)回響應(yīng).
一旦進(jìn)程啟動, register_contract/1
實(shí)例化器等待15毫秒,以確保其他進(jìn)程自己注冊
然后嘗試使用 regis_server
中的whereis
函數(shù)來檢索 Pid 并向進(jìn)程發(fā)送消息. 如果 regis 服務(wù)器運(yùn)行正常,
將收到一條消息,并且 Pid 將在函數(shù)底部的測試中匹配
不要喝太多的 kool-aid: 通過閱讀該測試,您已經(jīng)看到了我們必須做的小計時器工作,由于erlang 程序的并發(fā)性,和時間敏感性,
測試通常會被這樣的小型計時器填充,這些計時器的唯一作用是嘗試同步代碼.
然后問題就是嘗試定義一個好的定時器,延遲足夠長.
如果系統(tǒng)正在運(yùn)行高負(fù)荷的事情,計時器是否會等待足夠長的時間.
編寫測試的 erlang 程序員有時必須聰明,才能夠最大限度的減少他們需要他們需要多少同步才能使事情發(fā)揮作用
沒有更簡單的方案
接下來的測試介紹如下:
{"A list of registered processes can be obtained",
?setup(fun registered_list/1)}
因此,當(dāng)注冊了一對進(jìn)程,應(yīng)該可以獲得所有名稱的列表,這是一個 類似于 erlang 的 registed()
函數(shù)的調(diào)用
registered_list(_) ->
L1 = regis_server:get_names(),
Pids = [spawn(fun() -> callback(N) end) || N <- lists:seq(1,15)],
timer:sleep(200),
L2 = regis_server:get_names(),
[exit(Pid, kill) || Pid <- Pids],
[?_assertEqual([], L1),
?_assertEqual(lists:sort(lists:seq(1,15)), lists:sort(L2))].
首先,我們確保注冊進(jìn)程的第一個列表是空的 ?_assertEqual([],L1)
,這樣即使沒有任何進(jìn)程注冊自己,我們也能運(yùn)行.
然后創(chuàng)建了15個進(jìn)程,所有進(jìn)程都嘗試使用數(shù)字1...15注冊自己,我們讓測試睡眠一會兒,以確保所有進(jìn)程都有時間注冊自己
然后調(diào)用 regis_server:get_names()
.
名稱應(yīng)包含1到15之間的所有整數(shù),然后我們通過消除所有已注冊的進(jìn)程進(jìn)行輕微清晰,畢竟我不想泄漏他們
在測試集中使用測試之前,您會注意到測試在變量 L1和 L2 中存儲狀態(tài)的趨勢.
這樣做的原因是返回的測試集在測試啟動器與醒后很好的執(zhí)行
如果嘗試在?_assert*宏中放置依賴于其他進(jìn)程和時間明暗時間的函數(shù)調(diào)用,
你會讓一切都不同步,對于你和使用你的軟件的人來說,事情通常會很糟糕
下一個測試很簡單:
{"An undefined name should return 'undefined' to crash calls",
?setup(fun noregister/1)}
...
noregister(_) ->
[?_assertError(badarg, regis_server:whereis(make_ref()) ! hi),
?_assertEqual(undefined, regis_server:whereis(make_ref()))].
這與我們在本章前一節(jié)的演示中使用的測試幾乎相同.
在這一個中,我們只是想看看我們是否得到了正確的輸出,并且測試過程中,不能用不同的名稱注冊兩次
注意: 您可能已經(jīng)注意到上面的測試經(jīng)常使用 make_ref()
一大堆, 如果可能, 使用 make_ref
這樣的唯一值的函數(shù)很有用,
如果將來有人想要并行運(yùn)行測試或者在一個永不停止的 regis 服務(wù)下運(yùn)行他們,那么就可以這樣做而無需修改測試.
如果我們在所有測試中使用硬編碼名稱(如,a,b,c)
那么如果我們嘗試同時運(yùn)行多個測試套件,很可能遲早會發(fā)生名稱沖突,并非 regis_server_tests
套件中的所有測試都遵循此建議
主要用于演示目的/
接下來的測試與 two_names_one_pid
相反:
{"Two processes cannot share the same name",
?setup(fun two_pids_one_name/1)}].
...
two_pids_one_name(_) ->
Pid = spawn(fun() -> callback(myname) end),
timer:sleep(15),
Res = regis_server:register(myname, self()),
exit(Pid, kill),
[?_assertEqual({error, name_taken}, Res)].
這里,因為我們需要兩個進(jìn)程并且只需要其中一個進(jìn)程的結(jié)果,所以訣竅是產(chǎn)生一個進(jìn)程(我們不需要其結(jié)果的進(jìn)程),然后自己完成關(guān)鍵部分
您可以看到定時器用于確保其他進(jìn)程首先嘗試注冊名稱在 call_back/1
回調(diào)函數(shù)內(nèi)
并且測試本身等待輪到嘗試,因此期望出現(xiàn)錯誤元祖
這涵蓋了與流程注冊相關(guān)的測試的所有功能.
只留下與取消注冊進(jìn)程相關(guān)的那些:
unregister_test_() ->
[{"A process that was registered can be registered again iff it was "
"unregistered between both calls",
?setup(fun re_un_register/1)},
{"Unregistering never crashes",
?setup(fun unregister_nocrash/1)},
{"A crash unregisters a process",
?setup(fun crash_unregisters/1)}].
讓我們看看他們是如何實(shí)現(xiàn)的,第一個很簡單;
re_un_register(_) ->
Ref = make_ref(),
L = [regis_server:register(Ref, self()),
regis_server:register(make_ref(), self()),
regis_server:unregister(Ref),
regis_server:register(make_ref(), self())],
[?_assertEqual([ok, {error, already_named}, ok, ok], L)].
這種序列化列表中所有調(diào)用的方式是我在需要測試所有事件的結(jié)果時最喜歡做的一個很好的技巧.
通過將他們放入列表中,我可以將操作的順序與預(yù)期的列表進(jìn)行比價,看看情況如何
請注意,沒有任何指定 erlang 應(yīng)該按順序評估列表,但上面的技巧幾乎總是有效
以下測試,一個關(guān)于永不崩潰的測試,如下所示:
unregister_nocrash(_) ->
?_assertEqual(ok, regis_server:unregister(make_ref())).
哇,慢慢來,哥們. 就這樣吧,而已, 是的,如果你回顧一下 re_un_register
你會發(fā)現(xiàn)他已經(jīng)處理了對流程注銷的測試
對于 unregister_nocrash 我們真的只想知道它是否可以嘗試刪除不存在的進(jìn)程
然后是最后一個測試,也是您將擁有的任何測試注冊表中最終的測試之一:崩潰的命名進(jìn)程將具有未注冊的名稱
這有嚴(yán)重的影響,因為如果你沒有刪除名稱,你最終會有一個不斷增長的注冊表服務(wù),其名稱選擇越來越少
crash_unregisters(_) ->
Ref = make_ref(),
Pid = spawn(fun() -> callback(Ref) end),
timer:sleep(150),
Pid = regis_server:whereis(Ref),
exit(Pid, kill),
timer:sleep(95),
regis_server:register(Ref, self()),
S = regis_server:whereis(Ref),
Self = self(),
?_assertEqual(Self, S).
這個按順序讀取:
- 注冊進(jìn)程
- 確保該進(jìn)程已注冊
- 殺掉進(jìn)程
- 竊取進(jìn)程的id
- 檢查我們是偶自己擁有這個名稱
老實(shí)說,測試可以用更簡單的方式編寫:
crash_unregisters(_) ->
Ref = make_ref(),
Pid = spawn(fun() -> callback(Ref) end),
timer:sleep(150),
Pid = regis_server:whereis(Ref),
exit(Pid, kill),
?_assertEqual(undefined, regis_server:whereis(Ref)).
關(guān)于竊取死亡過程身份的整個部分只不過一個小偷的幻想.
而已. 如果你做得對,你應(yīng)該能夠編譯代碼并運(yùn)行測試
$ erl -make
Recompile: src/regis_sup
...
$ erl -pa ebin/
1> eunit:test(regis_server).
All 13 tests passed.
ok
2> eunit:test(regis_server, [verbose]).
======================== EUnit ========================
module 'regis_server'
module 'regis_server_tests'
The server can be started, stopped and has a registered name
regis_server_tests:49: is_registered...ok
regis_server_tests:50: is_registered...ok
[done in 0.006 s]
...
[done in 0.520 s]
=======================================================
All 13 tests passed.
ok
哦,是的,看看如何添加 verbose
選項,會將測試描述和運(yùn)行時信息添加到報告中,那很整齊
誰編織單元測試
在本章中,我們已經(jīng)了解如何使用 EUnit 的大多數(shù)功能,如何運(yùn)行寫在其中的套件,更重要的是, 我們已經(jīng)看到了一些與如何使用
在現(xiàn)實(shí)世界中有意義的模式編寫并發(fā)進(jìn)程測試相關(guān)的技術(shù)
應(yīng)該知道最后一個測試技巧:
當(dāng)您想要測試 gen_servers
和 gen_fsms
等流程時,您可能會想要檢查流程內(nèi)部的 state
, 這是一個很好的技巧,由 sys 模塊提供:
3> regis_server:start_link().
{ok,<0.160.0>}
4> regis_server:register(shell, self()).
ok
5> sys:get_status(whereis(regis_server)).
{status,<0.160.0>,
{module,gen_server},
[[{'$ancestors',[<0.31.0>]},
{'$initial_call',{regis_server,init,1}}],
running,<0.31.0>,[],
[{header,"Status for generic server regis_server"},
{data,[{"Status",running},
{"Parent",<0.31.0>},
{"Logged events",[]}]},
{data,[{"State",
{state,{1,{<0.31.0>,{shell,#Ref<0.0.0.333>},nil,nil}},
{1,{shell,{<0.31.0>,#Ref<0.0.0.333>},nil,nil}}}}]}]]}
整潔,對吧,與服務(wù)內(nèi)部相關(guān)的一切都是給你的:
你現(xiàn)在可以隨時檢查你需要的一切?
如果您對服務(wù)和諸如此類的東西感覺更舒服,建議您閱讀 為 Process Quests 的播放器模塊寫的測試
他們使用不同的技術(shù)測試 gen_server, 其中對 handle_call
/ handle_cast
/ handle_info
的所有單獨(dú)調(diào)用都是獨(dú)立嘗試的
無論如何,當(dāng)我們重寫流程注冊表以使用 ets 時,我們將看到測試的真正價值, ets 是一個可用于所有 erlang 進(jìn)程的內(nèi)存數(shù)據(jù)庫.