單元測(cè)試

erlang 單元測(cè)試

learn you some erlang for great good

歐盟國家委員會(huì)

測(cè)試的需要

我們編寫的軟件隨著時(shí)間的推移變的越來越大,越來越復(fù)雜.
當(dāng)發(fā)生這種情況時(shí),啟動(dòng) Erlang shell, 輸入內(nèi)容,查看結(jié)果以及確保代碼更改后,能夠正常工作.
隨著時(shí)間的推移,每個(gè)人都可以更加簡單的運(yùn)行之前準(zhǔn)備好的測(cè)試代碼,而不是一直按照清單去手工檢查軟件里的所有測(cè)試案例.
您也可能是測(cè)試驅(qū)動(dòng)開發(fā)的粉絲,因此也會(huì)發(fā)現(xiàn)測(cè)試很有用.

如果你還記得我們編寫 RPN 計(jì)算器的章節(jié),我們有一些手動(dòng)編寫的測(cè)試方法.
它們只是一組 Result = Expression 形式的模式匹配,如果出現(xiàn)問題就會(huì)崩潰,否則會(huì)成功.

這適用于您自己編寫的簡單代碼,但是當(dāng)我們進(jìn)行更嚴(yán)格的測(cè)試時(shí),我們肯定會(huì)想要更好的東西,比如:框架.

對(duì)于單元測(cè)試,我們傾向于堅(jiān)持 EUnit, 我們?cè)诒菊轮锌吹降?
對(duì)于集成測(cè)試, EUnit 和 Common Test 都可以完成這項(xiàng)工作.

事實(shí)上, Common Test 可以完成單元測(cè)試到系統(tǒng)測(cè)試,實(shí)質(zhì)是外部軟件測(cè)試的所有工作,而不是用 erlang 編寫的.
現(xiàn)在我們將使用 EUnit, 因?yàn)樗a(chǎn)生好的結(jié)果是那么簡單.

單元測(cè)試,什么是單元測(cè)試

EUnit,最簡單的形式,只是模塊中以 _test 結(jié)尾的自動(dòng)運(yùn)行的方法,就認(rèn)為他們是單元測(cè)試.
如果你去發(fā)掘我上面提到的 RPN 計(jì)算器,你會(huì)發(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.

這正是我們編寫的測(cè)試函數(shù),以確保計(jì)算器工作正常.找到之前的模塊,并嘗試運(yùn)行以下命令.

1> c(calc).
{ok,calc}
2> eunit:test(calc).
  Test passed.
ok

調(diào)用eunit:test(Module).正是我們需要的,是的,我們現(xiàn)在知道了 EUnit, 打開香檳,讓我們進(jìn)入一個(gè)不同的章節(jié).

顯然,只做這一點(diǎn)測(cè)試框架不會(huì)非常有用,而且在技術(shù)程序員的術(shù)語中,他可能被描述為"不太好".
EUnit 不僅僅是自動(dòng)導(dǎo)出和運(yùn)行以 _test()結(jié)尾的函數(shù).

例如,您可以將測(cè)試移動(dòng)到另一個(gè)模塊,以便您的代碼和測(cè)試不會(huì)混合在一起.
這意味著,您不能再測(cè)試私有函數(shù),但也意味著,如果您針對(duì)模塊的接口(導(dǎo)出的函數(shù))開發(fā)所有測(cè)試,那么在重構(gòu)代碼時(shí),不需要重寫測(cè)試.

讓我們嘗試用兩個(gè)簡單的模塊分離測(cè)試和代碼.

所以我們有 ops 和 ops_test, 其中第二個(gè)包括與第一個(gè)相關(guān)的測(cè)試.這是 EUnit 可以做的事情.

調(diào)用 eunit:test(Mod) 會(huì)自動(dòng)查找 Mod_tests 并在其中運(yùn)行測(cè)試.讓我們稍微改變測(cè)試(使其成為 3= ops:add(2,2)) 以查看失敗的樣子

我們可以看到測(cè)試失敗的結(jié)果和原因.
我們獲得了有關(guān)通過和失敗的測(cè)試數(shù)量的完整報(bào)告.雖然輸出很糟糕.

至少和普通的 Erlang 崩潰一樣糟糕:沒有行號(hào),沒有明確的解釋,并沒有確切的匹配什么內(nèi)容 等等.
偶爾們對(duì)運(yùn)行測(cè)試的測(cè)試框架感到無助,但并沒有告訴你太多關(guān)于他們的信息.
出于這個(gè)原因, EUnit 引入了一些宏來幫助我們.他們中的每一個(gè)都將為我們提供更清晰的報(bào)告(包括行號(hào))和更清晰的語義.
他們知道出了什么問題和知道出錯(cuò)的原因是不同的

?assert(Expression), ?assertNot(Expression)

將測(cè)試布爾值.如果除 true 之外的任何值傳入 ?assert ,將會(huì)展示錯(cuò)誤提示.同樣對(duì)于 ?assertNot,
對(duì)于不正確的值,這個(gè)宏相當(dāng)于執(zhí)行了 true =X 或者 false=Y.

?assertEqual(A, B)

在兩個(gè)表達(dá)式之間,進(jìn)行嚴(yán)格比較,相當(dāng)于=:=, 如果他們不等,將會(huì)提示失敗.這大致相當(dāng)于 true= X=:=Y.
從 R14B04版本開始,宏 ?assertNorEqual 是與 ?assertEqual 相反的操作

?assertMatch(Pattern, Expression)

這允許我們以類似于 Pattern = Expression 的形式進(jìn)行匹配,而不需要綁定變量.
這意味著我可以執(zhí)行類似的操作: ?aseertMatch({X,X},some_function()),并斷言我收到一個(gè)元素相同的元組.
而且,我以后可以做 ?assertMatch(X,Y), 并且 X不用綁定

這就是說,不用像 Pattern= Expression, 我們更接近于 (fun(Pattern) -> true; (_)-> erlang:error(nomatch) end)(Expressson):
模式中的變量頭,永遠(yuǎn)不會(huì)收到多個(gè)斷言的綁定.
?assertNotMatch在 EUnitR14B04版本中加入

?assertError(Pattern, Expression)

告訴 EUnit 表達(dá)式應(yīng)該導(dǎo)致錯(cuò)誤, 例如: ?assertError(badarith,1/0)將會(huì)是一次成功的測(cè)試

?assertThrow(Pattern, Expression)

?assertError 完全相同,但是使用 exit(Pattern) (并不是 exit/2)并不是erlang:error(Pattern)

?assertException(Class, Pattern, Expression)

先前三個(gè)宏一樣的形式,例如:?assertException(error,Pattern,Expression),與?assertError(Pattern,Expression)相同,從 R14B04版本開始,還有可以用于 ?assertNotException/3的宏

使用這些宏,我們可以再我們的模塊中編寫更好的測(cè)試

-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).

看看錯(cuò)誤報(bào)告有多好,我們知道 ops_tests第11行的 assertEqual 失敗了.
當(dāng)我們調(diào)用 ops:add(1,1),我們認(rèn)為會(huì)得到3作為值,但是我們得到2

當(dāng)然,您必須將這些值當(dāng)做 erlang 術(shù)語來讀,但至少他們?cè)谀抢?/p>

然而,令人討厭的是,即使我們有5個(gè)斷言,但是只有一個(gè)失敗,但整個(gè)測(cè)試仍然被視為失敗
如果知道某些斷言失敗,而沒有表現(xiàn)的所有其他斷言也失敗,那就更好了

我們的測(cè)試相當(dāng)于在學(xué)??荚?一旦你犯錯(cuò),你就會(huì)失敗并被拋棄.
然后你就像是一只死狗了,而且你只有一個(gè)可怕的一天了

測(cè)試生成器

由于這種對(duì)靈活性的共同需求, EUnit 支持成為測(cè)試生成器的東西.測(cè)試生成器非常簡單,可以用巧妙的方式在稍后運(yùn)行的函數(shù)中包含斷言.
我們將使用以 test() 結(jié)尾的函數(shù)和assertSomething 形式的宏,而不是以 _test()結(jié)尾的函數(shù)和?assertSomething 形式的宏

這些都是微小的變化,但他們使事情變得更加強(qiáng)大.以下兩個(gè)測(cè)試將是等效的.

function_test() -> ?assert(A == B).
function_test_() -> ?_assert(A == B).

這里, funtion_test_()被稱為測(cè)試生成器函數(shù),而 ?assert(A==B) 被稱作是測(cè)試生成器

他被稱為這樣,因?yàn)橹i底是,?_assert(A==B)的底層是現(xiàn)實(shí) fun()->?_assert(A==B) end. 也就是說生成測(cè)試的函數(shù)

與常規(guī)斷言相比,測(cè)試生成器的優(yōu)勢(shì)在于他們是 fun 函數(shù),這意味著,可以再不執(zhí)行的情況下操縱他們.
事實(shí)上,我們可以擁有以下形式的測(cè)試集

my_test_() ->
  [?_assert(A),
    [
      ?_assert(B), ?_assert(C), [?_assert(D)]
    ],
    [[?_assert(E)]]
  ].

測(cè)試集可以試測(cè)試生成器的深層嵌套列表.我們擁有可以返回測(cè)試函數(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))].

因?yàn)橹挥?add_test_()_ test_中結(jié)束,所以兩個(gè)函數(shù) test_them_Something()不會(huì)被視為測(cè)試.實(shí)際上,他們只會(huì)被 add_test_調(diào)用以生成測(cè)試.

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個(gè)測(cè)試跳到了7個(gè),測(cè)試生成器的魔力

如果我們只想測(cè)試套件的某些部分,也許只是 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

請(qǐng)注意,這僅適用于測(cè)試生成器功能,我們?cè)谶@里的 {generator,Fun} 就是 EUnit 的用法所謂的測(cè)試表示.我們還有一些其他表示

  • {module, Mod} : 運(yùn)行 module 中的所有測(cè)試
  • {dir, Path} : 運(yùn)行 path 中找到的所有模塊中的測(cè)試
  • {file, Path} : 運(yùn)行在單個(gè)編譯模塊中找到的所有測(cè)試
  • {generator, Fun} : 運(yùn)行單個(gè)生成器作為測(cè)試,如上所示
  • {application, AppName} : 運(yùn)行 AppName 的. app 文件中提到的所有模塊的所有測(cè)試方法

這些不同的測(cè)試表示,可以輕松地為整個(gè)應(yīng)用程序甚至版本運(yùn)行測(cè)試套件

裝置, 測(cè)試套件

test fixtur 名詞解釋

  • 一個(gè)test fixture 表示執(zhí)行一個(gè)或多個(gè)測(cè)試前的準(zhǔn)備工作,以及執(zhí)行完成后清理工作。
  • 例如:創(chuàng)建臨時(shí)或代理數(shù)據(jù)庫或目錄,或者是一個(gè)啟動(dòng)服務(wù)器進(jìn)程。
  • 也可以形象的理解為夾心餅干中的外面兩層
  • 成為測(cè)試夾具, 測(cè)試裝置,腳手架等,實(shí)在找不到合適的翻譯,姑且暫用本身 Fixtures

僅通過使用斷言和測(cè)試生成器來測(cè)試整個(gè)應(yīng)用程序,仍然非常困難.
這就是添加固定裝置的原因, Fixtures 雖然不是讓你的測(cè)試運(yùn)行到應(yīng)用程序級(jí)別的全能解決方案,但允許你圍繞測(cè)試建一個(gè)特定的腳手架.

所討論的腳手架是一種通用結(jié)構(gòu),允許我們?yōu)槊總€(gè)測(cè)試定義設(shè)置和拆卸功能.

這些函數(shù)將允許您構(gòu)建每個(gè)測(cè)試所需要的狀態(tài)和環(huán)境,此外,腳手架將允許您指定如何運(yùn)行測(cè)試,您想在本地,在單獨(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
一個(gè)不帶參數(shù)的函數(shù),每個(gè)測(cè)試都將傳遞 setup 函數(shù)返回的值

cleanup
一種函數(shù),他將 setup 函數(shù)的結(jié)果作為參數(shù),并負(fù)責(zé)清理所需的任何內(nèi)容..如果在 OTP 中終止與 init 相反,則 cleanup 功能和 setup 功能相反

Instantiator 實(shí)例化

她是一個(gè)獲取設(shè)置函數(shù)結(jié)果并返回測(cè)試集的函數(shù),請(qǐng)記住,測(cè)試集可能是深層嵌套的 ?_Macro 斷言列表

where
指定如何運(yùn)行測(cè)試, local,spawn,{spawn,node()}

好吧,那么在實(shí)踐中看起來像什么,好吧,讓我想象一下測(cè)試,以確保虛擬進(jìn)程注冊(cè)表正確處理嘗試注冊(cè)相同的進(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)].

這個(gè)腳手架,首先在 start/0函數(shù)內(nèi)啟動(dòng)注冊(cè)表服務(wù)器,然后調(diào)用實(shí)例化 two_names_one_pid(REsultFromSetup).
在那個(gè)測(cè)試中,我唯一要做的就是嘗試兩次注冊(cè)當(dāng)前進(jìn)程.

這就是實(shí)例化器工作的地方,第二次注冊(cè)的結(jié)果存儲(chǔ)在 Res 變量中,
然后,該函數(shù)將返回包含單個(gè)測(cè)試的測(cè)試集 (?assertEqual({error,already_named},Res)).

該測(cè)試集將由 EUnit 運(yùn)行,然后將調(diào)用 teardown 拆卸方法 stop/1將會(huì)被調(diào)用.使用 setup 函數(shù)返回的 pid, 它將能夠關(guān)閉我們事先啟動(dòng)的注冊(cè)表. 美好.

更好的是整個(gè)腳手架本身可以放在一個(gè)測(cè)試裝置中

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)他們始終相同時(shí),這就是第二種類型的腳手架,即 foreach 腳手架進(jìn)入舞臺(tái)的地方

{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 腳手架將獲取每個(gè)實(shí)例化器,并為每個(gè)實(shí)例化器運(yùn)行 setup 和 teardown 功能

現(xiàn)在我們知道如何為一個(gè)實(shí)例化器設(shè)置一個(gè)腳手架,然后為他們中的許多實(shí)例設(shè)置(每個(gè)實(shí)例都進(jìn)行setup 和 teardown 函數(shù)調(diào)用).
如果我想要一個(gè)setup 函數(shù)調(diào)用,并且一個(gè) teaddown 函數(shù),需要多個(gè)實(shí)例化器,該怎么辦

換句話說,如果我有很多實(shí)例化器,但我只想設(shè)置一些狀態(tài)呢. 對(duì)此有沒有簡單的辦法,但這可能是一個(gè)技巧

some_tricky_test_() ->
    {setup,
     fun start/0,
     fun stop/1,
     fun (SetupData) ->
        [some_instantiator1(SetupData),
         some_instantiator2(SetupData),
         ...
         some_instantiatorN(SetupData)]
     end}.

通過使用測(cè)試集可以是深層嵌套列表的事實(shí),我們將一對(duì)具有匿名函數(shù)實(shí)例化器包裝秤類似于他們的實(shí)例化器

當(dāng)您使用腳手架時(shí),測(cè)試還可以對(duì)他們應(yīng)該如何運(yùn)行進(jìn)行更精細(xì)的控制,有四種選擇

  1. {spawn, TestSet}
    • 在主要測(cè)試過程之外的單獨(dú)過程中運(yùn)行測(cè)試
    • 測(cè)試過程將等待所有生成的測(cè)試完成
  2. {timeout, Seconds, TestSet}
    • 測(cè)試將運(yùn)行數(shù)秒,如果他們花費(fèi)時(shí)間超出,他們將被終止,而不會(huì)更加輕松
  3. {inorder, TestSet}
    • 這告訴 EUnit 嚴(yán)格按照返回的順序在測(cè)試集中運(yùn)行測(cè)試
  4. {inparallel, Tests}
    • 在可能的情形下,測(cè)試將并行運(yùn)行

作為例子, some_tricky_test_/0 測(cè)試生成器可以重寫為下面這樣

some_tricky2_test_() ->
    {setup,
     fun start/0,
     fun stop/1,
     fun(SetupData) ->
       {inparallel,
        [some_instantiator1(SetupData),
         some_instantiator2(SetupData),
         ...
         some_instantiatorN(SetupData)]}
     end}.

這對(duì)于腳手架來說,真的是大部,但是現(xiàn)在還有一個(gè)我忘了展示的好玩法.
您可以以一種簡潔的方法描述測(cè)試,看一下這個(gè)

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}}.

很好,對(duì)吧,你可以通過執(zhí)行{Comment,Fixture}來包裝腳手架,以獲得可讀性,讓我們把它付諸實(shí)踐吧

注冊(cè)測(cè)試

因?yàn)橹皇强吹缴厦娴奶摷贉y(cè)試并不是最有趣的事情,并且以為假裝測(cè)試不存在的軟件更糟糕
我們將研究我為 regis-1.0.0進(jìn)程注冊(cè)表編寫的測(cè)試, 這是 Process Quest 使用的測(cè)試

現(xiàn)在, regis 的開發(fā)是以測(cè)試驅(qū)動(dòng)的方式完成的. 希望你不討厭 TDD(測(cè)試驅(qū)動(dòng)開發(fā)),但即使你這樣做,也不應(yīng)該太糟糕,因?yàn)槭潞?br> 我們會(huì)看看測(cè)試套件

通過這樣做,我們切斷了一些試錯(cuò)序列,并且我可能已經(jīng)第一次編寫了它,并且由于文本編輯的魔力,我看起來真的很稱職.
regis 應(yīng)用程序由三個(gè)過程組成:

  • 一個(gè)監(jiān)督者,一個(gè)主 server, 和一個(gè)application 回調(diào)模塊.
  • 我們知道 supervisor 只會(huì)檢查 server, 并且回調(diào)除了作為兩個(gè)模塊之間的接口外,什么都不做

我們可以安全的編寫一個(gè)專注于server 本身的測(cè)試套件,而不需要任何外部依賴

作為一名優(yōu)秀的 TDD 粉絲,我首先編寫了一分我想要涵蓋的所有功能的列表:

  • 尊重類似于 Erlang 默認(rèn)進(jìn)程注冊(cè)表的接口
  • server 將具有注冊(cè)名稱,以便可以在不跟蹤其pid 的情況下聯(lián)系到它
  • 可以通過我們的服務(wù)注冊(cè)流程,然后可以通過其名稱和它聯(lián)系
  • 可以獲得所有已經(jīng)注冊(cè)進(jìn)程的列表
  • 任何進(jìn)程都沒有注冊(cè)的名稱應(yīng)該返回 undefined, 一邊使用它們來崩潰
  • 一個(gè)進(jìn)程不能有兩個(gè)名字
  • 兩個(gè)進(jìn)程不能共享同一個(gè)名字
  • 如果已經(jīng)在調(diào)用前取消注冊(cè),可以再次注冊(cè)已經(jīng)注冊(cè)的進(jìn)程
  • 取消注冊(cè)進(jìn)程永遠(yuǎn)不會(huì)崩潰
  • 注冊(cè)進(jìn)程崩潰將會(huì)取消注冊(cè)名稱

這是一個(gè)值得尊敬的名單.逐個(gè)完成元素并按照我的方式添加案例,我將每個(gè)規(guī)范轉(zhuǎn)換為測(cè)試.獲得最終的文件是 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)模塊是空時(shí)看起來很奇怪,但是當(dāng)你填滿它,他會(huì)越來越有意義
添加第一個(gè)測(cè)試后,最初的測(cè)試是應(yīng)該可以啟動(dò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)好多了
文件頂部僅包含腳手架和功能的頂級(jí)描述
第二部分,包含我們可能需要的裝置和清理功能,我們最后一個(gè)包返回測(cè)試集的實(shí)例化器

在這種情況下,實(shí)例化器會(huì)檢查 regis_server:start_link() 是否生成了一個(gè)真正存活的進(jìn)程,
并且它是使用名稱 regis_server 注冊(cè)的.如果這是真的,那么這將適用于服務(wù)

如果我們查看文件的當(dāng)前版本,她現(xiàn)在看起來更像是這兩個(gè)第一部分.

-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

不錯(cuò),不是嗎. 請(qǐng)注意,在我編寫套件時(shí),我最終看到我從不需要任何其他設(shè)置和拆卸功能,而不是 start/0stop/1
出于這個(gè)原因,我添加了 set_up 宏,這使得事情看起來必有腳手架都要完全展開要好一些,
現(xiàn)在很明顯,我將功能列表的每個(gè)點(diǎn)都變成了一堆測(cè)試
你會(huì)注意到我根據(jù)他們是否與啟動(dòng)和停止有關(guān),注冊(cè)進(jìn)程和取消進(jìn)程.

通過閱讀測(cè)試生成器的定義,我們可以知道該模塊應(yīng)該做什么.測(cè)試生成文檔(盡管他們不應(yīng)取代適當(dāng)?shù)奈臋n)

我們將稍微研究一下測(cè)試,看看事情是以某種方式完成.列表 start_stop_test_/0中的第一個(gè)測(cè)試,只需要注冊(cè)服務(wù)

start_stop_test_() ->
    {"The server can be started, stopped and has a registered name",
     ?setup(fun is_registered/1)}.

測(cè)試本身的實(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)我們查看測(cè)試的第一個(gè)版本時(shí),會(huì)檢查該過程是否可用.
雖然函數(shù) erlang:is_process_alive(Pid) 對(duì)你來說可能不熟悉,但 沒有什么特別之處.
顧名思義,他會(huì)檢查進(jìn)程當(dāng)前是否正在運(yùn)行.

我把那個(gè)測(cè)試放在那里的原因很簡單,一旦我們啟動(dòng)它很可能服務(wù)器崩潰,或者它從未在第一個(gè)時(shí)間開始崩潰,我們不希望這樣
第二個(gè)測(cè)試與能夠注冊(cè)流程有關(guān)

{"A process can be registered and contacted",
 ?setup(fun register_contact/1)}

這是測(cè)試代碼的樣子:

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)雅的測(cè)試,他的作用是產(chǎn)生一個(gè)過程,它只會(huì)注冊(cè)自己并回復(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)程啟動(dòng), register_contract/1 實(shí)例化器等待15毫秒,以確保其他進(jìn)程自己注冊(cè)
然后嘗試使用 regis_server 中的whereis 函數(shù)來檢索 Pid 并向進(jìn)程發(fā)送消息. 如果 regis 服務(wù)器運(yùn)行正常,
將收到一條消息,并且 Pid 將在函數(shù)底部的測(cè)試中匹配

不要喝太多的 kool-aid: 通過閱讀該測(cè)試,您已經(jīng)看到了我們必須做的小計(jì)時(shí)器工作,由于erlang 程序的并發(fā)性,和時(shí)間敏感性,
測(cè)試通常會(huì)被這樣的小型計(jì)時(shí)器填充,這些計(jì)時(shí)器的唯一作用是嘗試同步代碼.
然后問題就是嘗試定義一個(gè)好的定時(shí)器,延遲足夠長.
如果系統(tǒng)正在運(yùn)行高負(fù)荷的事情,計(jì)時(shí)器是否會(huì)等待足夠長的時(shí)間.
編寫測(cè)試的 erlang 程序員有時(shí)必須聰明,才能夠最大限度的減少他們需要他們需要多少同步才能使事情發(fā)揮作用
沒有更簡單的方案

接下來的測(cè)試介紹如下:

{"A list of registered processes can be obtained",
 ?setup(fun registered_list/1)}

因此,當(dāng)注冊(cè)了一對(duì)進(jìn)程,應(yīng)該可以獲得所有名稱的列表,這是一個(gè) 類似于 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))].

首先,我們確保注冊(cè)進(jìn)程的第一個(gè)列表是空的 ?_assertEqual([],L1),這樣即使沒有任何進(jìn)程注冊(cè)自己,我們也能運(yùn)行.
然后創(chuàng)建了15個(gè)進(jìn)程,所有進(jìn)程都嘗試使用數(shù)字1...15注冊(cè)自己,我們讓測(cè)試睡眠一會(huì)兒,以確保所有進(jìn)程都有時(shí)間注冊(cè)自己
然后調(diào)用 regis_server:get_names().

名稱應(yīng)包含1到15之間的所有整數(shù),然后我們通過消除所有已注冊(cè)的進(jìn)程進(jìn)行輕微清晰,畢竟我不想泄漏他們
在測(cè)試集中使用測(cè)試之前,您會(huì)注意到測(cè)試在變量 L1和 L2 中存儲(chǔ)狀態(tài)的趨勢(shì).
這樣做的原因是返回的測(cè)試集在測(cè)試啟動(dòng)器與醒后很好的執(zhí)行
如果嘗試在?_assert*宏中放置依賴于其他進(jìn)程和時(shí)間明暗時(shí)間的函數(shù)調(diào)用,
你會(huì)讓一切都不同步,對(duì)于你和使用你的軟件的人來說,事情通常會(huì)很糟糕

下一個(gè)測(cè)試很簡單:

{"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()))].

這與我們?cè)诒菊虑耙还?jié)的演示中使用的測(cè)試幾乎相同.
在這一個(gè)中,我們只是想看看我們是否得到了正確的輸出,并且測(cè)試過程中,不能用不同的名稱注冊(cè)兩次

注意: 您可能已經(jīng)注意到上面的測(cè)試經(jīng)常使用 make_ref() 一大堆, 如果可能, 使用 make_ref 這樣的唯一值的函數(shù)很有用,
如果將來有人想要并行運(yùn)行測(cè)試或者在一個(gè)永不停止的 regis 服務(wù)下運(yùn)行他們,那么就可以這樣做而無需修改測(cè)試.
如果我們?cè)谒袦y(cè)試中使用硬編碼名稱(如,a,b,c)
那么如果我們嘗試同時(shí)運(yùn)行多個(gè)測(cè)試套件,很可能遲早會(huì)發(fā)生名稱沖突,并非 regis_server_tests 套件中的所有測(cè)試都遵循此建議
主要用于演示目的/

接下來的測(cè)試與 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)].

這里,因?yàn)槲覀冃枰獌蓚€(gè)進(jìn)程并且只需要其中一個(gè)進(jìn)程的結(jié)果,所以訣竅是產(chǎn)生一個(gè)進(jìn)程(我們不需要其結(jié)果的進(jìn)程),然后自己完成關(guān)鍵部分

您可以看到定時(shí)器用于確保其他進(jìn)程首先嘗試注冊(cè)名稱在 call_back/1回調(diào)函數(shù)內(nèi)
并且測(cè)試本身等待輪到嘗試,因此期望出現(xiàn)錯(cuò)誤元祖
這涵蓋了與流程注冊(cè)相關(guān)的測(cè)試的所有功能.
只留下與取消注冊(cè)進(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)的,第一個(gè)很簡單;

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)用的方式是我在需要測(cè)試所有事件的結(jié)果時(shí)最喜歡做的一個(gè)很好的技巧.
通過將他們放入列表中,我可以將操作的順序與預(yù)期的列表進(jìn)行比價(jià),看看情況如何

請(qǐng)注意,沒有任何指定 erlang 應(yīng)該按順序評(píng)估列表,但上面的技巧幾乎總是有效

以下測(cè)試,一個(gè)關(guān)于永不崩潰的測(cè)試,如下所示:

unregister_nocrash(_) ->
    ?_assertEqual(ok, regis_server:unregister(make_ref())).

哇,慢慢來,哥們. 就這樣吧,而已, 是的,如果你回顧一下 re_un_register 你會(huì)發(fā)現(xiàn)他已經(jīng)處理了對(duì)流程注銷的測(cè)試
對(duì)于 unregister_nocrash 我們真的只想知道它是否可以嘗試刪除不存在的進(jìn)程

然后是最后一個(gè)測(cè)試,也是您將擁有的任何測(cè)試注冊(cè)表中最終的測(cè)試之一:崩潰的命名進(jìn)程將具有未注冊(cè)的名稱
這有嚴(yán)重的影響,因?yàn)槿绻銢]有刪除名稱,你最終會(huì)有一個(gè)不斷增長的注冊(cè)表服務(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).

這個(gè)按順序讀取:

  • 注冊(cè)進(jìn)程
  • 確保該進(jìn)程已注冊(cè)
  • 殺掉進(jìn)程
  • 竊取進(jìn)程的id
  • 檢查我們是偶自己擁有這個(gè)名稱

老實(shí)說,測(cè)試可以用更簡單的方式編寫:

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)于竊取死亡過程身份的整個(gè)部分只不過一個(gè)小偷的幻想.
而已. 如果你做得對(duì),你應(yīng)該能夠編譯代碼并運(yùn)行測(cè)試

$ 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 選項(xiàng),會(huì)將測(cè)試描述和運(yùn)行時(shí)信息添加到報(bào)告中,那很整齊

誰編織單元測(cè)試

在本章中,我們已經(jīng)了解如何使用 EUnit 的大多數(shù)功能,如何運(yùn)行寫在其中的套件,更重要的是, 我們已經(jīng)看到了一些與如何使用
在現(xiàn)實(shí)世界中有意義的模式編寫并發(fā)進(jìn)程測(cè)試相關(guān)的技術(shù)

應(yīng)該知道最后一個(gè)測(cè)試技巧:
當(dāng)您想要測(cè)試 gen_serversgen_fsms 等流程時(shí),您可能會(huì)想要檢查流程內(nèi)部的 state, 這是一個(gè)很好的技巧,由 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}}}}]}]]}

整潔,對(duì)吧,與服務(wù)內(nèi)部相關(guān)的一切都是給你的:
你現(xiàn)在可以隨時(shí)檢查你需要的一切?
如果您對(duì)服務(wù)和諸如此類的東西感覺更舒服,建議您閱讀 為 Process Quests 的播放器模塊寫的測(cè)試

他們使用不同的技術(shù)測(cè)試 gen_server, 其中對(duì) handle_call / handle_cast / handle_info 的所有單獨(dú)調(diào)用都是獨(dú)立嘗試的
無論如何,當(dāng)我們重寫流程注冊(cè)表以使用 ets 時(shí),我們將看到測(cè)試的真正價(jià)值, ets 是一個(gè)可用于所有 erlang 進(jìn)程的內(nèi)存數(shù)據(jù)庫.

引自http://www.itdecent.cn/p/b6856a15478a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容