單元測試

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ì)的控制,有四種選擇

  1. {spawn, TestSet}
    • 在主要測試過程之外的單獨(dú)過程中運(yùn)行測試
    • 測試過程將等待所有生成的測試完成
  2. {timeout, Seconds, TestSet}
    • 測試將運(yùn)行數(shù)秒,如果他們花費(fèi)時間超出,他們將被終止,而不會更加輕松
  3. {inorder, TestSet}
    • 這告訴 EUnit 嚴(yán)格按照返回的順序在測試集中運(yùn)行測試
  4. {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/0stop/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_serversgen_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ù)庫.

引自http://www.reibang.com/p/b6856a15478a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末论熙,一起剝皮案震驚了整個濱河市富俄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌截歉,老刑警劉巖胖腾,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瘪松,居然都是意外死亡咸作,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門宵睦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來记罚,“玉大人,你說我怎么就攤上這事状飞『潦ぃ” “怎么了书斜?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵诬辈,是天一觀的道長。 經(jīng)常有香客問我荐吉,道長焙糟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任样屠,我火速辦了婚禮穿撮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘痪欲。我一直安慰自己悦穿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布业踢。 她就那樣靜靜地躺著栗柒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪知举。 梳的紋絲不亂的頭發(fā)上瞬沦,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音雇锡,去河邊找鬼逛钻。 笑死,一個胖子當(dāng)著我的面吹牛锰提,可吹牛的內(nèi)容都是我干的曙痘。 我是一名探鬼主播芳悲,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼屡江!你這毒婦竟也來了芭概?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惩嘉,失蹤者是張志新(化名)和其女友劉穎罢洲,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體文黎,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惹苗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了耸峭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桩蓉。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖劳闹,靈堂內(nèi)的尸體忽然破棺而出院究,到底是詐尸還是另有隱情,我是刑警寧澤本涕,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布业汰,位于F島的核電站,受9級特大地震影響菩颖,放射性物質(zhì)發(fā)生泄漏样漆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一晦闰、第九天 我趴在偏房一處隱蔽的房頂上張望放祟。 院中可真熱鬧,春花似錦呻右、人聲如沸跪妥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眉撵。三九已至,卻和暖如春醒串,著一層夾襖步出監(jiān)牢的瞬間执桌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工芜赌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仰挣,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓缠沈,卻偏偏與公主長得像膘壶,于是被迫代替她去往敵國和親错蝴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容