檢視原始碼 撰寫測試套件

對測試套件作者的支援

ct 模組提供了撰寫測試案例的主要介面。這包括例如以下幾點:

  • 用於列印和記錄的函式
  • 用於讀取組態資料的函式
  • 用於以錯誤原因終止測試案例的函式
  • 用於將註解新增至 HTML 概觀頁面的函式

有關這些函式的詳細資訊,請參閱 ct 模組。

Common Test 應用程式也包含其他名為 ct_<component> 的模組,這些模組提供各種支援,主要簡化了 RPC、SNMP、FTP、Telnet 等通訊協定的使用。

測試套件

測試套件是一個包含測試案例的普通 Erlang 模組。建議模組的名稱採用 *_SUITE.erl 的格式。否則,Common Test 中的目錄和自動編譯函式將無法找到它(至少預設情況下無法找到)。

也建議在所有測試套件模組中包含 ct.hrl 標頭檔。

每個測試套件模組都必須匯出函式 all/0,該函式會傳回要在該模組中執行的所有測試案例群組和測試案例的清單。

測試套件要實作的回呼函式都列在 ct_suite 模組中。它們也會在本使用手冊稍後的章節中更詳細地說明。

每個套件的初始化和結束

每個測試套件模組都可以包含選用的組態函式 init_per_suite/1end_per_suite/1。如果定義了初始化函式,則也必須定義結束函式。

如果存在 init_per_suite,則會在執行測試案例之前先呼叫它。它通常包含套件中所有測試案例通用的初始化,這些初始化僅執行一次。init_per_suite 建議用於在受測系統 (SUT) 或 Common Test 主機節點或兩者上設定和驗證狀態和環境,以便套件中的測試案例正確執行。以下是初始組態操作的範例:

  • 開啟與 SUT 的連線
  • 初始化資料庫
  • 執行安裝腳本

end_per_suite 會在測試套件執行階段的最後階段呼叫(在最後一個測試案例完成後)。此函式旨在用於清除 init_per_suite 後的狀態。

init_per_suiteend_per_suite 會在專用的 Erlang 程序上執行,就像測試案例一樣。然而,這些函式的結果不包含在成功、失敗和跳過案例的測試執行統計資料中。

init_per_suite 的引數是 Config,也就是每個測試案例作為輸入引數的相同的執行階段組態資料的鍵值清單。init_per_suite 可以使用測試案例需要的資訊來修改此參數。經過修改的 Config 清單是函式的傳回值。

如果 init_per_suite 失敗,則會自動跳過測試套件中的所有測試案例(稱為自動跳過),包括 end_per_suite

請注意,如果套件中不存在 init_per_suiteend_per_suite,則 Common Test 會改為呼叫虛擬函式(名稱相同),以便可以將 hook 函式產生的輸出儲存到這些虛擬函式的記錄檔中。如需詳細資訊,請參閱 Common Test Hooks

每個測試案例的初始化和結束

每個測試套件模組都可以包含選用的組態函式 init_per_testcase/2end_per_testcase/2。如果定義了初始化函式,則也必須定義結束函式。

如果存在 init_per_testcase,則會在套件中的每個測試案例之前呼叫它。它通常包含必須為每個測試案例完成的初始化(類似於套件的 init_per_suite)。

end_per_testcase/2 會在每個測試案例完成後呼叫,以便在 init_per_testcase 之後進行清除。

注意

但是,如果 end_per_testcase 崩潰,則測試結果不受影響。同時,測試執行記錄中會報告此事件。

這些函式的第一個引數是測試案例的名稱。此值可以與函式子句中的模式比對或條件運算式搭配使用,以針對不同的測試案例選擇不同的初始化和清除常式,或針對許多或所有測試案例執行相同的常式。

第二個引數是執行階段組態資料的 Config 鍵值清單,其值與 init_per_suite 傳回的清單相同。init_per_testcase/2 可以修改此參數或傳回「原樣」。init_per_testcase/2 的傳回值會作為參數 Config 傳遞至測試案例本身。

測試伺服器會忽略 end_per_testcase/2 的傳回值,但 save_configfail 元組除外。

end_per_testcase 可以檢查測試案例是否成功。(這又可以決定如何執行清除)。這是透過從 Config 讀取標記為 tc_status 的值來完成的。值為下列其中一個:

  • ok

  • {failed,Reason}

    其中 Reasontimetrap_timeout、來自 exit/1 的資訊,或執行階段錯誤的詳細資料

  • {skipped,Reason}

    其中 Reason 是使用者特定的詞彙

如果測試案例因為呼叫 ct:abort_current_testcase/1 或在時間陷阱逾時後終止,則甚至會呼叫函式 end_per_testcase/2。然而,end_per_testcase 接著會在與測試案例函式不同的程序上執行。在這種情況下,end_per_testcase 無法透過傳回 {fail,Reason} 或使用 {save_config,Data} 儲存資料來變更測試案例終止的原因。

在以下兩種情況下會跳過測試案例:

  • 如果 init_per_testcase 崩潰(稱為自動跳過)。
  • 如果 init_per_testcase 傳回元組 {skip,Reason}(稱為使用者跳過)。

也可以透過從 init_per_testcase 傳回元組 {fail,Reason},將測試案例標記為失敗而不執行它。

注意

如果 init_per_testcase 崩潰,或傳回 {skip,Reason}{fail,Reason},則不會呼叫函式 end_per_testcase

如果在執行 end_per_testcase 期間判斷要將成功測試案例的狀態變更為失敗,則 end_per_testcase 可以傳回元組 {fail,Reason}(其中 Reason 說明測試案例失敗的原因)。

由於 init_per_testcaseend_per_testcase 與測試案例在相同的 Erlang 程序上執行,因此這些組態函式的列印輸出會包含在測試案例記錄檔中。

測試案例

測試伺服器關注的最小單位是測試案例。每個測試案例都可以測試許多事項,例如,使用不同的參數多次呼叫相同的介面函式。

作者可以選擇在每個測試案例中放入許多或少量測試。以下是一些需要注意的事項:

  • 許多小型測試案例往往會導致額外且可能重複的程式碼,以及因為初始化和清除的巨大額外負荷而導致測試執行緩慢。避免重複的程式碼,例如,使用共用的協助函式。否則,產生的套件會變得難以閱讀和理解,且維護成本高昂。
  • 較大的測試案例會讓您在失敗時更難以判斷發生了什麼問題。此外,當發生錯誤時,可能會跳過大部分測試程式碼。
  • 當測試案例變得太大且太廣泛時,可讀性和可維護性會受到影響。不確定產生的記錄檔是否能很好地反映執行測試的次數。

測試案例函式接受一個引數 Config,其中包含組態資訊,例如 data_dirpriv_dir。(如需有關這些資訊的詳細資訊,請參閱 資料和私有目錄 一節)。在呼叫時,Config 的值與稍早提及的 init_per_testcase 的傳回值相同。

注意

測試案例函式引數 Config 不應與可以從組態檔中擷取的資訊混淆(使用 ct:get_config/1/2)。測試案例引數 Config 用於測試套件和測試案例的執行階段組態,而組態檔則用於包含與 SUT 相關的資料。這兩種組態資料的處理方式不同。

由於參數 Config 是一個鍵值對組成的列表,也就是一種稱為屬性列表的資料類型,因此可以使用 proplists 模組來處理。例如,可以使用 proplists:get_value/2 函式來搜尋並傳回值。此外,或是作為替代方案,通用的 lists 模組也包含有用的函式。通常,對 Config 執行的唯一操作是插入(將組加入列表的頭部)和查找。若要在組態中查找值,可以使用 proplists:get_value。例如:PrivDir = proplists:get_value(priv_dir, Config)

測試案例的結果可以透過幾種方式自訂。詳細資訊請參閱 Module:Testcase/1 的手冊,該手冊位於 ct_suite 模組中。

測試案例資訊函式

對於每個測試案例函式,可以有一個額外的函式,名稱相同但沒有參數。這就是測試案例資訊函式。它預期會傳回一個帶有標籤的元組列表,指定有關測試案例的各種屬性。

下列標籤具有特殊含義

  • timetrap - 設定允許測試案例執行的最長時間。如果超過此時間,測試案例將因原因 timetrap_timeout 而失敗。請注意,init_per_testcaseend_per_testcase 包含在 time trap 時間內。詳細資訊請參閱 Timetrap 超時 章節。

  • userdata - 指定與測試案例相關的任何資料。可以使用 ct:userdata/3 工具函式隨時擷取此資料。

  • silent_connections - 詳細資訊請參閱 靜默連線 章節。

  • require - 指定測試案例所需的組態變數。如果在任何測試系統組態檔中都找不到所需的組態變數,則會跳過該測試案例。

    如果變數在任何組態檔中都找不到,則可以為必要的變數提供預設值。若要指定預設值,請將一個形式為 {default_config,ConfigVariableName,Value} 的元組新增至測試案例資訊列表(列表中位置無關緊要)。

    範例

    testcase1() ->
        [{require, ftp},
         {default_config, ftp, [{ftp, "my_ftp_host"},
                                {username, "aladdin"},
                                {password, "sesame"}]}}].
    testcase2() ->
        [{require, unix_telnet, unix},
         {require, {unix, [telnet, username, password]}},
         {default_config, unix, [{telnet, "my_telnet_host"},
                                 {username, "aladdin"},
                                 {password, "sesame"}]}}].

有關 require 的詳細資訊,請參閱外部組態資料章節中的 需要和讀取組態資料 和函式 ct:require/1/2

注意

為必要的變數指定預設值可能會導致測試案例始終被執行。這可能不是預期的行為。

如果沒有針對特定測試案例具體設定 timetraprequire,或兩者皆無,則會使用函式 suite/0 指定的預設值。

除了前面提到的標籤之外,其他標籤都會被測試伺服器忽略。

以下是測試案例資訊函式的範例

reboot_node() ->
    [
     {timetrap,{seconds,60}},
     {require,interfaces},
     {userdata,
         [{description,"System Upgrade: RpuAddition Normal RebootNode"},
          {fts,"http://someserver.ericsson.se/test_doc4711.pdf"}]}
    ].

測試套件資訊函式

例如,可以在測試套件模組中使用函式 suite/0 來設定預設的 timetrap 值並 require 外部組態資料。如果測試案例或群組資訊函式也指定了任何資訊標籤,則會覆寫 suite/0 設定的預設值。詳細資訊請參閱 測試案例資訊函式測試案例群組

也可以使用套件資訊列表指定以下選項

以下是套件資訊函式的範例

suite() ->
    [
     {timetrap,{minutes,10}},
     {require,global_names},
     {userdata,[{info,"This suite tests database transactions."}]},
     {silent_connections,[telnet]},
     {stylesheet,"db_testing.css"}
    ].

測試案例群組

測試案例群組是一組共享組態函式和執行屬性的測試案例。測試案例群組由函式 groups/0 定義,該函式應傳回具有下列語法的 term

groups() -> GroupDefs

Types:

GroupDefs = [GroupDef]
GroupDef = {GroupName,Properties,GroupsAndTestCases}
GroupName = atom()
GroupsAndTestCases = [GroupDef | {group,GroupName} | TestCase |
                     {testcase,TestCase,TCRepeatProps}]
TestCase = atom()
TCRepeatProps = [{repeat,N} | {repeat_until_ok,N} | {repeat_until_fail,N}]

GroupName 是群組的名稱,且在測試套件模組中必須是唯一的。群組可以巢狀結構,方法是在另一個群組的 GroupsAndTestCases 列表中包含群組定義。Properties 是群組的執行屬性列表。可能的值如下

Properties = [parallel | sequence | Shuffle | {GroupRepeatType,N}]
Shuffle = shuffle | {shuffle,Seed}
Seed = {integer(),integer(),integer()}
GroupRepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
                  repeat_until_any_ok | repeat_until_any_fail
N = integer() | forever

說明

  • parallel - Common Test 會並行執行群組中的所有測試案例。

  • sequence - 案例會按照「測試案例和套件之間的依賴關係」章節中 序列 章節的描述依序執行。

  • shuffle - 群組中的案例會以隨機順序執行。

  • repeatrepeat_until_* - 指示 Common Test 重複執行群組中的所有案例指定的次數,或直到任何或所有案例失敗或成功。

範例

groups() -> [{group1, [parallel], [test1a,test1b]},
             {group2, [shuffle,sequence], [test2a,test2b,test2c]}].

若要指定群組的執行順序(也包括不屬於任何群組的測試案例),請將 {group,GroupName} 形式的元組新增至 all/0 列表。

範例

all() -> [testcase1, {group,group1}, {testcase,testcase2,[{repeat,10}]}, {group,group2}].

執行屬性與 all/0 中的群組元組:也可以指定 {group,GroupName,Properties}。這些屬性會覆寫在群組定義中指定的屬性(請參閱先前的 groups/0)。這樣,可以執行相同的測試集,但使用不同的屬性,而無需複製相關群組定義。

如果群組包含子群組,則也可以在群組元組中指定這些子群組的執行屬性:{group,GroupName,Properties,SubGroups}。其中,SubGroups 是元組列表,{GroupName,Properties}{GroupName,Properties,SubGroups} 代表子群組。在 groups/0 中針對群組定義,但未在 SubGroups 列表中指定的任何子群組,都會使用其預定義的屬性執行。

範例

groups() -> [{tests1, [], [{tests2, [], [t2a,t2b]},
                          {tests3, [], [t31,t3b]}]}].

若要使用不同的 tests2 屬性來執行群組 tests1 兩次

all() ->
   [{group, tests1, default, [{tests2, [parallel]}]},
    {group, tests1, default, [{tests2, [shuffle,{repeat,10}]}]}].

這相當於下列規格

all() ->
   [{group, tests1, default, [{tests2, [parallel]},
                              {tests3, default}]},
    {group, tests1, default, [{tests2, [shuffle,{repeat,10}]},
                              {tests3, default}]}].

default 表示將使用預定義的屬性。

以下範例說明如何在具有深度巢狀群組的案例中覆寫屬性

groups() ->
   [{tests1, [], [{group, tests2}]},
    {tests2, [], [{group, tests3}]},
    {tests3, [{repeat,2}], [t3a,t3b,t3c]}].

all() ->
   [{group, tests1, default,
     [{tests2, default,
       [{tests3, [parallel,{repeat,100}]}]}]}].

為了易於閱讀,所有語法定義都可以替換為函式呼叫,函式呼叫的傳回值應符合預期的語法案例。

範例

all() ->
   [{group, tests1, default, test_cases()},
    {group, tests1, default, [shuffle_test(),
                              {tests3, default}]}].
test_cases() ->
   [{tests2, [parallel]}, {tests3, default}].

shuffle_test() ->
   {tests2, [shuffle,{repeat,10}]}.

在測試規格中也可以使用描述的語法,以便在執行時變更群組屬性,而無需編輯測試套件。詳細資訊請參閱「執行測試和分析結果」章節中的 測試規格 章節。

如圖所示,屬性可以組合使用。例如,如果同時指定了 shufflerepeat_until_any_failsequence,則會重複且以隨機順序執行群組中的測試案例,直到測試案例失敗。然後立即停止執行並跳過剩餘的案例。

在群組執行開始之前,會呼叫組態函式 init_per_group(GroupName, Config)。從此函式傳回的元組列表會以通常的方式透過參數 Config 傳遞給測試案例。init_per_group/2 旨在用於群組中測試案例通用的初始化。在群組執行完成後,會呼叫函式 end_per_group(GroupName, Config)。此函式旨在用於在 init_per_group/2 之後進行清理。如果定義了 init 函式,則必須同時定義 end 函式。

每當執行群組時,如果套件中不存在 init_per_groupend_per_group,則 Common Test 會改為呼叫虛擬函式(名稱相同)。鉤子函式產生的輸出會儲存到這些虛擬函式的記錄檔中。詳細資訊請參閱「Common Test 鉤子」章節中的 操作測試 章節。

注意

無論案例是否屬於群組,都總是會針對每個個別的測試案例呼叫 init_per_testcase/2end_per_testcase/2

群組的屬性會始終列印在 init_per_group/2 的 HTML 記錄頂部。群組的總執行時間會包含在 end_per_group/2 的記錄底部。

測試案例群組可以是巢狀結構,因此可以使用相同的 init_per_group/2end_per_group/2 函式來組態群組集。巢狀群組可以透過在另一個群組的測試案例列表中包含群組定義或群組名稱參考來定義。

範例

groups() -> [{group1, [shuffle], [test1a,
                                  {group2, [], [test2a,test2b]},
                                  test1b]},
             {group3, [], [{group,group4},
                           {group,group5}]},
             {group4, [parallel], [test4a,test4b]},
             {group5, [sequence], [test5a,test5b,test5c]}].

在先前的範例中,如果 all/0[{group,group1},{group,group3}] 的順序傳回群組名稱參考,則組態函式和測試案例的順序如下(請注意,init_per_testcase/2end_per_testcase/2: 也總是會被呼叫,但在本範例中為簡化而未包含)

init_per_group(group1, Config) -> Config1  (*)
     test1a(Config1)
     init_per_group(group2, Config1) -> Config2
          test2a(Config2), test2b(Config2)
     end_per_group(group2, Config2)
     test1b(Config1)
end_per_group(group1, Config1)
init_per_group(group3, Config) -> Config3
     init_per_group(group4, Config3) -> Config4
          test4a(Config4), test4b(Config4)  (**)
     end_per_group(group4, Config4)
     init_per_group(group5, Config3) -> Config5
          test5a(Config5), test5b(Config5), test5c(Config5)
     end_per_group(group5, Config5)
end_per_group(group3, Config3)

(*) 測試案例 test1atest1bgroup2 的順序未定義,因為 group1 具有 shuffle 屬性。

(**) 這些案例不會依序執行,而是並行執行。

屬性不會從頂層群組繼承到巢狀子群組。例如,在先前的範例中,group2 中的測試案例不會以隨機順序執行(這是 group1 的屬性)。

平行屬性與巢狀群組

如果群組具有平行屬性,其測試案例會同時產生並行執行。然而,測試案例不允許與 end_per_group/2 平行執行,這表示執行平行群組的時間等於群組中最慢的測試案例的執行時間。平行執行測試案例的一個負面影響是,在群組的 end_per_group/2 函數完成之前,HTML 摘要頁面不會更新個別測試案例日誌的連結。

在平行群組下巢狀的群組會與先前的(平行)測試案例平行開始執行(無論巢狀群組具有哪些屬性)。然而,由於測試案例永遠不會與相同群組的 init_per_group/2end_per_group/2 平行執行,因此只有在巢狀群組完成後,先前群組中剩餘的平行案例才會產生。

平行測試案例與 I/O

平行測試案例具有私有的 I/O 伺服器作為其群組領導者。(關於群組領導者概念的說明,請參閱 ERTS)。中央 I/O 伺服器程序,處理來自常規測試案例和配置函數的輸出,在平行群組執行期間不會回應 I/O 訊息。這對於理解以避免某些陷阱非常重要,例如以下情況:

如果在執行期間(例如 init_per_suite/1)產生程序 P,它會繼承 init_per_suite 程序的群組領導者。此群組領導者是先前提到過的中央 I/O 伺服器程序。如果在稍後的時間,在平行測試案例執行期間,某些事件觸發程序 P 呼叫 io:format/1/2,該呼叫永遠不會返回(因為群組領導者處於無回應狀態),並導致 P 掛起。

重複群組

測試案例群組可以重複特定的次數(由整數指定)或無限次重複(由 forever 指定)。如果任何或所有案例失敗或成功,也可以過早停止重複,也就是說,如果使用了任何屬性 repeat_until_any_failrepeat_until_any_okrepeat_until_all_failrepeat_until_all_ok。如果使用了基本的 repeat 屬性,則測試案例的狀態與重複操作無關。

子群組的狀態可以返回(okfailed),以影響上一層群組的執行。這是透過在 end_per_group/2 中,查找 Config 列表中的 tc_group_properties 值,並檢查群組中測試案例的結果來完成的。如果要從群組返回狀態 failed 作為結果,則 end_per_group/2 應返回值 {return_group_result,failed}。當評估是否要重複執行群組時,Common Test 會考慮子群組的狀態(除非使用了基本的 repeat 屬性)。

tc_group_properties 的值是狀態元組的列表,每個元組都具有鍵 okskippedfailed。狀態元組的值是一個列表,其中包含已執行並具有對應狀態結果的測試案例名稱。

以下是如何從群組返回狀態的範例

end_per_group(_Group, Config) ->
    Status = proplists:get_value(tc_group_result, Config),
    case proplists:get_value(failed, Status) of
        [] ->                                   % no failed cases
            {return_group_result,ok};
        _Failed ->                              % one or more failed
            {return_group_result,failed}
    end.

end_per_group/2 中,也可以檢查子群組的狀態(或許是為了決定當前群組要返回的狀態)。這就像先前範例中說明的那樣簡單,只是群組名稱儲存在元組 {group_result,GroupName} 中,可以在狀態列表中搜尋到它。

範例

end_per_group(group1, Config) ->
    Status = proplists:get_value(tc_group_result, Config),
    Failed = proplists:get_value(failed, Status),
    case lists:member({group_result,group2}, Failed) of
          true ->
              {return_group_result,failed};
          false ->
              {return_group_result,ok}
    end;
...

注意

當重複測試案例群組時,配置函數 init_per_group/2end_per_group/2 也總是在每次重複時呼叫。

混亂的測試案例順序

在正常情況下,群組中測試案例的執行順序與群組定義中測試案例列表指定的順序相同。但是,如果設定了屬性 shuffleCommon Test 會改為以隨機順序執行測試案例。

您可以使用混亂屬性 {shuffle,Seed} 提供種子值(三個整數的元組)。這樣,每次執行群組時都可以建立相同的混亂順序。如果未指定種子值,Common Test 會為混亂操作建立「隨機」種子(使用 erlang:timestamp/0 的回傳值)。種子值總是會印到 init_per_group/2 日誌檔案中,以便可以用於在後續測試執行中重新建立相同的執行順序。

注意

如果重複一個混亂的測試案例群組,則在回合之間不會重設種子。

如果在具有 shuffle 屬性的群組中指定子群組,則此子群組相對於群組中測試案例(和其他子群組)的執行順序是隨機的。但是,子群組中測試案例的順序不是隨機的(除非子群組具有 shuffle 屬性)。

群組資訊函數

測試案例群組資訊函數 group(GroupName) 的作用與先前描述的套件和測試案例資訊函數相同。但是,群組資訊函數的範圍是相關群組(GroupName)中的所有測試案例和子群組。

範例

group(connection_tests) ->
   [{require,login_data},
    {timetrap,1000}].

群組資訊屬性會覆寫使用套件資訊函數設定的屬性,並且可以被測試案例資訊屬性覆寫。有關有效資訊屬性和更多一般資訊的列表,請參閱 測試案例資訊函數

用於 Init 和 End 配置的資訊函數

資訊函數也可以用於函數 init_per_suiteend_per_suiteinit_per_groupend_per_group,它們的工作方式與 測試案例資訊函數 相同。這很有用,例如,用於設定時間陷阱和要求僅與相關配置函數相關的外部配置數據(而不影響為套件中的群組和測試案例設定的屬性)。

資訊函數 init/end_per_suite() 會針對 init/end_per_suite(Config) 呼叫,而資訊函數 init/end_per_group(GroupName) 會針對 init/end_per_group(GroupName,Config) 呼叫。但是,資訊函數不能與 init/end_per_testcase(TestCase, Config) 一起使用,因為這些配置函數在測試案例程序上執行,並使用與測試案例相同的屬性(也就是說,由測試案例資訊函數 TestCase() 設定的屬性)。有關有效資訊屬性和更多一般資訊的列表,請參閱 測試案例資訊函數

資料與私有目錄

在資料目錄 data_dir 中,測試模組有其測試所需的文件。data_dir 的名稱是測試套件的名稱,後接 "_data"。例如,"some_path/foo_SUITE.beam" 的資料目錄為 "some_path/foo_SUITE_data/"。為了可移植性,請使用此目錄,也就是說,避免在您的套件中硬編碼目錄名稱。由於資料目錄儲存在與您的測試套件相同的目錄中,即使您的測試套件目錄的路徑在測試套件實作和執行之間發生變更,您也可以依賴它在執行時的存在。

priv_dir 是測試案例的私有目錄。每當測試案例(或配置函數)需要寫入檔案時,都可以使用此目錄。私有目錄的名稱由 Common Test 產生,它也會建立該目錄。

預設情況下,Common Test 為每次測試執行建立一個中央私有目錄,由所有測試案例共用。這並不總是合適的。尤其是當相同的測試案例在測試執行期間執行多次時(也就是說,如果它們屬於具有屬性 repeat 的測試案例群組),並且存在私有目錄中的檔案被覆寫的風險。在這些情況下,可以配置 Common Test 為每個測試案例和執行建立一個專用的私有目錄。這是透過使用旗標/選項 create_priv_dir 來完成的(與 ct_run 程式、ct:run_test/1 函數或作為測試規格術語使用)。此選項有三個可能的值如下:

  • auto_per_run
  • auto_per_tc
  • manual_per_tc

第一個值表示預設的 priv_dir 行為,也就是每次測試執行會建立一個私有目錄。後兩個值則告訴 Common Test 針對每個測試案例和執行產生唯一的測試目錄名稱。如果使用自動版本,則會自動建立所有私有目錄。對於具有許多測試案例或重複執行,或兩者都有的測試執行,這可能會變得非常沒有效率。因此,如果改為使用手動版本,測試案例必須在需要時告訴 Common Test 建立 priv_dir。它會呼叫函式 ct:make_priv_dir/0 來執行此操作。

注意

請勿依賴目前的工作目錄來讀取和寫入資料檔案,因為這是不可攜的。所有暫存檔都應寫入 priv_dir 中,而所有資料檔案都應位於 data_dir 中。此外,Common Test 伺服器會在每個案例開始時,將目前的工作目錄設定為測試案例的日誌目錄。

執行環境

每個測試案例都由專用的 Erlang 程序執行。程序會在測試案例開始時產生,並在測試案例結束時終止。組態函式 init_per_testcaseend_per_testcase 會在與測試案例相同的程序上執行。

組態函式 init_per_suiteend_per_suite 與測試案例一樣,會在專用的 Erlang 程序上執行。

Timetrap 超時

測試案例的預設時間限制為 30 分鐘,除非由套件、群組或測試案例資訊函式指定了 timetrap。由 suite/0 定義的 timetrap 超時值是套件中每個測試案例(以及組態函式 init_per_suite/1end_per_suite/1init_per_group/2end_per_group/2)所使用的值。由 group(GroupName) 定義的 timetrap 值會覆寫由 suite() 定義的值,並用於群組 GroupName 及其任何子群組中的每個測試案例。如果子群組的 group/1 定義了 timetrap 值,它會覆寫其較高層級群組的 timetrap 值。由個別測試案例(透過測試案例資訊函式)設定的 timetrap 值會覆寫群組和套件層級的 timetrap 值。

在測試案例或組態函式執行期間,也可以動態設定或重設 timetrap。這是透過呼叫 ct:timetrap/1 完成的。此函式會取消目前的 timetrap 並啟動新的 timetrap(該 timetrap 會保持作用中狀態,直到逾時或目前函式結束)。

可以使用選項 multiply_timetraps 在啟動時指定乘數值來擴展 timetrap 值。也可以讓測試伺服器決定自動放大 timetrap 超時值。也就是說,如果在測試期間執行諸如 covertrace 之類的工具。此功能預設為停用,可以使用啟動選項 scale_timetraps 來啟用。

如果測試案例需要暫停一段時間,該時間也會乘以 multiply_timetraps(並且如果啟用 scale_timetraps,則也可能會放大),則可以使用函式 ct:sleep/1(而不是例如 timer:sleep/1)。

函式(fun/0{Mod,Func,Args} (MFA) 元組)可以在套件、群組和測試案例資訊函式中指定為 timetrap 值,並作為函式 ct:timetrap/1 的引數。

範例

{timetrap,{my_test_utils,timetrap,[?MODULE,system_start]}}

ct:timetrap(fun() -> my_timetrap(TestCaseName, Config) end)

使用者 timetrap 函式可用於以下兩種用途:

  • 作為 timetrap。當函式傳回時,會觸發逾時。
  • 傳回 timetrap 時間值(而不是函式)。

在執行 timetrap 函式(在平行專用的 timetrap 程序上執行)之前,Common Test 會取消先前為測試案例或組態函式設定的任何計時器。當 timetrap 函式傳回時,會觸發逾時,除非傳回值是有效的 timetrap 時間,例如整數或 {SecMinOrHourTag,Time} 元組(詳細資訊,請參閱模組 ct_suite)。如果傳回時間值,則會啟動新的 timetrap,以在指定時間後產生逾時。

使用者 timetrap 函式可以在延遲後傳回時間值。有效的 timetrap 時間是延遲時間加上傳回的時間。

記錄 - 類別和詳細程度

Common Test 提供以下三個主要函式來列印字串:

  • ct:log(Category, Importance, Format, FormatArgs, Opts)
  • ct:print(Category, Importance, Format, FormatArgs)
  • ct:pal(Category, Importance, Format, FormatArgs)

log/1,2,3,4,5 函式將字串列印到測試案例日誌檔案。print/1,2,3,4 函式將字串列印到螢幕。pal/1,2,3,4 函式將相同的字串同時列印到檔案和螢幕。這些函式在模組 ct 中說明。

可選的 Category 引數可用於將記錄輸出分類。類別可用於以下兩種用途:

  • 將輸出的重要性與特定的詳細程度進行比較。
  • 根據使用者特定的 HTML 樣式表 (CSS) 來格式化輸出。

引數 Importance 指定一個重要性層級,與詳細程度(一般和/或每個類別設定)相比,該層級決定輸出是否可見。Importance 是 0..99 範圍內的任何整數。ct.hrl 標頭檔中存在預定義的常數。預設重要性層級 ?STD_IMPORTANCE(如果未提供引數 Importance 則使用)為 50。這也是用於標準 I/O 的重要性,例如,從使用 io:format/2io:put_chars/1 等進行的列印輸出。

Importance 與由 verbosity 啟動旗標/選項設定的詳細程度進行比較。層級可以每個類別設定,或一般設定,或兩者都設定。如果使用者未設定 verbosity,則會使用 100 的層級(?MAX_VERBOSITY = 所有輸出可見)作為預設值。Common Test 會執行以下測試:

Importance >= (100-VerbosityLevel)

常數 ?STD_VERBOSITY 的值為 50(請參閱 ct.hrl)。在此層級,會列印所有標準 I/O。如果設定較低的詳細程度,則會忽略標準 I/O 輸出。詳細程度 0 會有效地關閉所有記錄(除了 Common Test 本身所做的輸出)。

一般詳細程度不與任何特定類別相關聯。此層級會設定標準 I/O 輸出、未分類的 ct:log/print/pal 輸出以及未定義詳細程度的類別的閾值。

範例

測試案例執行期間的一些輸出:

io:format("1. Standard IO, importance = ~w~n", [?STD_IMPORTANCE]),
ct:log("2. Uncategorized, importance = ~w", [?STD_IMPORTANCE]),
 ct:log(info, "3. Categorized info, importance = ~w", [?STD_IMPORTANCE]),
 ct:log(info, ?LOW_IMPORTANCE, "4. Categorized info, importance = ~w", [?LOW_IMPORTANCE]),
 ct:log(error, ?HI_IMPORTANCE, "5. Categorized error, importance = ~w", [?HI_IMPORTANCE]),
 ct:log(error, ?MAX_IMPORTANCE, "6. Categorized error, importance = ~w", [?MAX_IMPORTANCE]),

如果使用 50 的一般詳細程度(?STD_VERBOSITY)啟動測試:

$ ct_run -verbosity 50

則會列印以下內容:

1. Standard IO, importance = 50
2. Uncategorized, importance = 50
3. Categorized info, importance = 50
5. Categorized error, importance = 75
6. Categorized error, importance = 99

如果使用以下方式啟動測試:

$ ct_run -verbosity 1 and info 75

則會列印以下內容:

3. Categorized info, importance = 50
4. Categorized info, importance = 25
6. Categorized error, importance = 99

請注意,為了僅指定輸出的重要性,不需要類別引數。範例:

ct:pal(?LOW_IMPORTANCE, "Info report: ~p", [Info])

或者可能與常數結合使用:

-define(INFO, ?LOW_IMPORTANCE).
-define(ERROR, ?HI_IMPORTANCE).

ct:log(?INFO, "Info report: ~p", [Info])
ct:pal(?ERROR, "Error report: ~p", [Error])

可以使用函式 ct:set_verbosity/2ct:get_verbosity/1 在測試執行期間修改和讀取詳細程度。

ct:log/print/pal 中的引數 FormatFormatArgs 始終傳遞給 STDLIB 函式 io:format/3(詳細資訊,請參閱 io 手冊頁)。

ct:pal/4ct:log/5 會將標頭新增至列印到日誌檔案的字串。這些字串也包裝在具有 CSS 類別屬性的 div 標籤中,以便可以套用樣式表格式。若要停用輸出的此功能(也就是說,要取得類似於使用 io:format/2 的結果),請使用 no_css 選項呼叫 ct:log/5

如何在「執行測試和分析結果」章節的 HTML 樣式表一節中說明如何將類別對應到 CSS 標籤。

Common Test 會逸出使用 ct:pal/4io:format/2 列印到日誌檔案的輸出中的特殊 HTML 字元(<、> 和 &)。若要將具有 HTML 標籤的字串列印到日誌,請使用 ct:log/3,4,5 函式。字元逸出功能預設針對 ct:log/3,4,5 停用,但可以使用 Opts 清單中的 esc_chars 選項啟用,請參閱 ct:log/3,4,5

如果需要停用字元逸出功能(通常是為了向後相容性),請使用 ct_run 啟動旗標 -no_esc_charsct:run_test/1 啟動選項 {esc_chars,Bool}(此啟動選項在測試規格中也支援)。

有關日誌檔案的詳細資訊,請參閱「執行測試和分析結果」章節中的 日誌檔案一節。

非法相依性

即使使用 Common Test 架構編寫測試套件非常有效率,仍然可能會犯錯,主要是因為非法相依性。以下是我們自己在執行 Erlang/OTP 測試套件時,根據自身經驗發現的一些較常犯的錯誤:

  • 相依於目前目錄並在其中寫入:

    這是測試套件中常見的錯誤。假設目前目錄與作者開發測試案例時使用的目前目錄相同。許多測試案例甚至嘗試將暫存檔寫入此目錄。相反地,應使用 data_dirpriv_dir 來尋找資料和寫入暫存檔。

  • 相依於執行順序:

    在開發測試套件期間,請勿對測試案例或套件的執行順序做出假設。例如,測試案例不得假設其所相依的伺服器已由先前的測試案例啟動。原因如下:

    • 使用者/操作員可以隨意指定順序,有時不同的執行順序可能更相關或更有效率。
    • 如果使用者為測試指定一整個測試套件目錄,則套件的執行順序取決於作業系統如何列出檔案,這在不同系統之間會有所不同。
    • 如果使用者只想執行測試套件的子集,則一個測試案例不可能成功地依賴另一個測試案例。
  • 依賴 Unix 系統

    透過 os:cmd 執行 Unix 命令可能無法在非 Unix 平台上運作。

  • 巢狀測試案例

    從另一個測試案例啟動一個測試案例,不僅會重複測試相同的內容,也會使追蹤測試內容變得更加困難。此外,如果被呼叫的測試案例因某種原因失敗,呼叫者也會失敗。這樣一來,一個錯誤會導致多個錯誤報告,這應該避免。

    許多測試案例函數通用的功能可以在共用的幫助函數中實現。如果這些函數對跨套件的測試案例有用,請將幫助函數放入共用的幫助模組中。

  • 發生錯誤時未能崩潰或退出

    如果測試案例稍後失敗,則在未檢查返回值是否表示成功的情況下發出請求是可以接受的,但僅列印錯誤訊息(到記錄檔中)並成功返回是絕對不能接受的。這樣的測試案例是有害的,因為它們在檢視測試結果時會產生虛假的安全性。

  • 對後續測試案例造成混亂

    測試案例應盡可能還原執行環境,以便後續測試案例不會因其執行順序而崩潰。函數 end_per_testcase 適用於此。