檢視原始碼 通用測試鉤子

概觀

通用測試鉤子 (CTH) 框架允許使用在所有測試套件呼叫之前和之後的鉤子,來擴展 Common Test 的預設行為。CTH 允許進階的 Common Test 使用者抽象出多個測試套件共有的行為,而無需在所有測試套件中都加入程式庫呼叫。這可以用於記錄、啟動和監控外部系統、建構測試所需的 C 檔案等等。

簡而言之,CTH 允許您執行以下操作:

  • 在每次套件設定呼叫之前操作執行階段設定。
  • 操作所有套件設定呼叫的回傳值,並延伸操作測試本身的結果。

以下章節將說明如何使用 CTH、它們何時執行,以及如何在 CTH 中操作測試結果。

警告

在 CTH 內執行時,所有時間陷阱都會關閉。因此,如果您的 CTH 永遠不回傳,整個測試執行將會停滯。

安裝 CTH

CTH 可以透過多種方式在您的測試執行中安裝。您可以為執行中的所有測試、特定的測試套件,以及測試套件內的特定群組執行此操作。如果您希望在測試執行中的所有測試套件中都存在 CTH,則可以透過以下三種方式來完成:

  • -ct_hooks 作為 ct_run 的引數新增。若要使用此方法新增多個 CTH,請使用關鍵字 and 將它們附加在一起,例如 ct_run -ct_hooks cth1 [{debug,true}] and cth2 ...
  • 將標籤 ct_hooks 新增至您的測試規格
  • 將標籤 ct_hooks 新增至您對 ct:run_test/1 的呼叫。

也可以在測試套件中新增 CTH。這可以透過在來自 suite/0init_per_suite/1init_per_group/2 的設定列表中回傳 {ct_hooks,[CTH]} 來完成。

在這種情況下,CTH 可以只是 CTH 的模組名稱,也可以是包含模組名稱和初始引數的元組,還可以選擇 CTH 的鉤子優先權。例如,以下其中一種:

  • {ct_hooks,[my_cth_module]}
  • {ct_hooks,[{my_cth_module,[{debug,true}]}]}
  • {ct_hooks,[{my_cth_module,[{debug,true}],500}]}

請注意,無論您如何安裝 CTH,其 BEAM 檔案都必須在 Common Test 執行時位於程式碼路徑中。ct_run 接受 -pa 命令列選項。

覆寫 CTH

預設情況下,每次安裝 CTH 都會啟動它的一個新實例。如果您想要在測試規格中覆寫 CTH,同時又希望它們存在於套件資訊函式中,這可能會導致問題。id/1 回呼的存在是為了解決此問題。透過在兩個地方回傳相同的 idCommon Test 知道此 CTH 已安裝,並且不會嘗試再次安裝它。

CTH 執行順序

預設情況下,每個已安裝的 CTH 都會按照它們安裝的順序執行初始化呼叫,然後反轉執行結束呼叫。此順序可以稱為以測試為中心,因為在測試案例執行後順序會反轉,並且對應於 ct_hooks_order 選項的預設值 (test)。

並非總是需要以安裝為基礎的順序,因此 Common Test 允許使用者為每個鉤子指定優先權。可以在 CTH 函式 init/2 中或在安裝鉤子時指定優先權。安裝時指定的優先權會覆寫 CTH 回傳的優先權。

在某些情況下,並非需要所有結束呼叫的反轉順序,而是使用者可能偏好後鉤呼叫的反轉順序。可以使用具有 config 值的 ct_hooks_order 選項來啟用此行為。啟用此選項後,執行順序以設定為中心,因為反轉順序發生在每個設定函式之後,而不是與測試案例相關。

請注意,ct_hooks_order 選項被視為全域框架設定。如果多次設定該選項,框架將僅處理第一個值。

ct_hooks_order 選項可以設定為:ct_run 引數、測試規格或 suite/0 回傳值。

CTH 範圍

一旦 CTH 安裝到特定測試執行中,它就會一直存在,直到其範圍過期。CTH 的範圍取決於它的安裝時間,請參閱下表。函式 init/2 在範圍開始時呼叫,而函式 terminate/1 在範圍結束時呼叫。

CTH 安裝在CTH 範圍開始於之前CTH 範圍結束於之後
ct_run第一個測試套件要執行最後一個測試套件已執行
ct:run_test執行第一個測試套件最後一個測試套件已執行
測試規格執行第一個測試套件最後一個測試套件已執行
suite/0呼叫 pre_init_per_suite/3已針對該測試套件呼叫 post_end_per_suite/4
init_per_suite/1呼叫 post_init_per_suite/4已針對該測試套件呼叫 post_end_per_suite/4
init_per_group/2呼叫 post_init_per_group/5已針對該群組呼叫 post_end_per_group/5

表格:CTH 的範圍

CTH 處理程序和表格

CTH 的執行方式與一般測試套件的處理程序範圍相同,也就是說,不同的處理程序會執行 init_per_suite 鉤子,然後執行 init_per_groupper_testcase 鉤子。因此,如果您想在 CTH 中產生一個處理程序,您無法與 CTH 處理程序連結,因為它會在後鉤結束後退出。此外,如果您因為某種原因需要一個帶有 CTH 的 ETS 表格,您必須產生一個處理該表格的處理程序。

外部設定資料和記錄

可以透過呼叫 ct:get_config/1,2,3 來讀取 CTH 中的設定資料值(如 要求和讀取設定資料 區段中所述)。所討論的設定變數必須一如既往地先由套件、群組或測試案例資訊函式,或透過函式 ct:require/1/2 來要求。後者也可以在 CT 鉤子函式中使用。

CT 鉤子函式可以呼叫 ct 介面中的任何記錄函式,將資訊列印到記錄檔,或在套件概觀頁面中新增註解。

操作測試

透過 CTH 可以操作測試和設定函式的結果。使用 CTH 執行此操作的主要目的是允許從測試套件中抽象出常見模式,並將其應用於多個測試套件,而無需複製任何程式碼。CTH 的所有回呼函式都遵循以下所述的通用介面。

Common Test 總是會呼叫所有可用的鉤子函式,即使是套件中未實作的設定函式的 pre- 和 post 鉤子也是如此。例如,即使測試套件 x_SUITE 沒有匯出 init_per_suite/1,也會為它呼叫 pre_init_per_suite(x_SUITE, ...)post_init_per_suite(x_SUITE, ...)。透過此功能,鉤子可以用作設定的回退,並且所有設定函式都可以替換為鉤子函式。

前鉤子

在 CTH 中,可以在以下函式之前掛鉤行為:

這可以在名為 pre_<函式名稱> 的 CTH 函式中完成。這些函式會採用引數 SuiteNameName(群組或測試案例名稱,如果適用)、ConfigCTHState。CTH 函式的回傳值始終是套件/群組/測試結果和更新的 CTHState 的組合。

若要讓測試套件繼續執行,請回傳您希望測試使用的設定列表作為結果。

除了 pre_end_per_testcase/4 以外的所有前鉤子,都可以透過回傳包含 skipfail 的元組,以及作為結果的原因,來略過或使測試失敗。

範例

pre_init_per_suite(SuiteName, Config, CTHState) ->
  case db:connect() of
    {error,_Reason} ->
      {{fail, "Could not connect to DB"}, CTHState};
    {ok, Handle} ->
      {[{db_handle, Handle} | Config], CTHState#state{ handle = Handle }}
  end.

注意

如果您使用多個 CTH,則返回元組的第一部分會作為下一個 CTH 的輸入。因此在先前的範例中,下一個 CTH 可以將 {fail,Reason} 作為第二個參數。如果有很多 CTH 相互作用,請不要讓每個 CTH 都返回 failskip。相反地,透過 Config 列表返回要採取的動作,並實作一個 CTH,在最後採取正確的動作。

後置鉤子

在 CTH 中,行為可以在下列函數之後進行掛鉤:

這是在名為 post_<函數名稱> 的 CTH 函數中完成的。這些函數會接收參數 SuiteNameName (群組或測試案例名稱,如果適用)、ConfigReturnCTHState。在這裡的 Config 與測試案例呼叫時所用的 Config 相同。Return 是測試案例返回的值。如果測試案例因崩潰而失敗,則 Return{'EXIT',{{Error,Reason},Stacktrace}}

CTH 函數的返回值始終是套件/群組/測試的結果和更新後的 CTHState 的組合。如果您不希望回呼影響測試結果,請返回提供給 CTH 的 Return 資料。您也可以修改測試結果。透過移除包含 tc_status 元素的 Config 列表,您可以從測試失敗中恢復。與所有前置鉤子一樣,也可以在後置鉤子中讓測試案例失敗/跳過。

範例

post_end_per_testcase(_Suite, _TC, Config, {'EXIT',{_,_}}, CTHState) ->
  case db:check_consistency() of
    true ->
      %% DB is good, pass the test.
      {proplists:delete(tc_status, Config), CTHState};
    false ->
      %% DB is not good, mark as skipped instead of failing
      {{skip, "DB is inconsistent!"}, CTHState}
  end;
post_end_per_testcase(_Suite, _TC, Config, Return, CTHState) ->
  %% Do nothing if tc does not crash.
  {Return, CTHState}.

注意

僅在萬不得已的情況下才使用 CTH 從測試案例失敗中恢復。如果使用不當,將很難確定哪些測試在測試執行中通過或失敗。

跳過和失敗鉤子

在針對所有已安裝的 CTH 執行任何後置鉤子後,如果測試案例失敗或被跳過,則會分別呼叫 on_tc_failon_tc_skip。在這一點上,您無法進一步影響測試結果。

將外部使用者應用程式與 Common Test 同步

CTH 可用於將測試執行與外部使用者應用程式同步。例如,init 函數可以啟動和/或與一個應用程式通信,該應用程式的目的是為即將進行的測試執行準備 SUT,或初始化資料庫以在測試執行期間儲存測試資料。同樣地,terminate 函數可以命令此類應用程式在測試執行後重置 SUT,和/或告知應用程式結束活動的工作階段並終止。在 init 或終止階段產生的任何系統錯誤或進度報告都會儲存在 測試前和測試後 I/O 記錄中。(這也適用於使用 ct:log/2ct:pal/2 進行的任何輸出)。

為了確保 Common Test 不會在外部應用程式準備就緒之前開始執行測試,或關閉其記錄檔並關機,Common Test 可以與應用程式同步。在啟動和關機期間,可以暫停 Common Test,只需讓 CTH 在 init 或終止函數中評估 receive 運算式即可。巨集 ?CT_HOOK_INIT_PROCESS (執行鉤子 init 函數的進程) 和 ?CT_HOOK_TERMINATE_PROCESS (執行鉤子終止函數的進程) 各自指定要傳送訊息的正確 Common Test 進程的名稱。這樣做是為了從 receive 返回。這些巨集定義在 ct.hrl 中。

CTH 範例

下列 CTH 將關於測試執行的資訊記錄成可由 file:consult/1 (在 Kernel 中) 解析的格式

%%% Common Test Example Common Test Hook module.
%%%
%%% To use this hook, on the command line:
%%%     ct_run -suite example_SUITE -pa . -ct_hooks example_cth
%%%
%%% Note `-pa .`: the hook beam file must be in the code path when installing.
-module(example_cth).

%% Mandatory Callbacks
-export([init/2]).

%% Optional Callbacks
-export([id/1]).

-export([pre_init_per_suite/3]).
-export([post_end_per_suite/4]).

-export([pre_init_per_testcase/4]).
-export([post_end_per_testcase/5]).

-export([on_tc_skip/4]).

-export([terminate/1]).

%% This hook state is threaded through all the callbacks.
-record(state, {filename, total, suite_total, ts, tcs, data, skipped}).
%% This example hook prints its results to a file, see terminate/1.
-record(test_run, {total, skipped, suites}).

%% Return a unique id for this CTH.
%% Using the filename means the hook can be used with different
%% log files to separate timing data within the same test run.
%% See Installing a CTH for more information.
id(Opts) ->
    %% the path is relative to the test run directory
    proplists:get_value(filename, Opts, "example_cth.log").

%% Always called before any other callback function. Use this to initiate
%% any common state.
init(Id, _Opts) ->
    {ok, #state{filename = Id, total = 0, data = []}}.

%% Called before init_per_suite is called.
pre_init_per_suite(_Suite,Config,State) ->
    {Config, State#state{suite_total = 0, tcs = []}}.

%% Called after end_per_suite.
post_end_per_suite(Suite,_Config,Return,State) ->
    Data = {suites, Suite, State#state.suite_total,
            lists:reverse(State#state.tcs)},
    {Return, State#state{data = [Data | State#state.data],
                         total = State#state.total + State#state.suite_total}}.

%% Called before each init_per_testcase.
pre_init_per_testcase(_Suite,_TC,Config,State) ->
    Now = erlang:monotonic_time(microsecond),
    {Config, State#state{ts = Now, suite_total = State#state.suite_total + 1}}.

%% Called after each end_per_testcase.
post_end_per_testcase(Suite,TC,_Config,Return,State) ->
    Now = erlang:monotonic_time(microsecond),
    TCInfo = {testcase, Suite, TC, Return, Now - State#state.ts},
    {Return, State#state{ts = undefined, tcs = [TCInfo | State#state.tcs]}}.

%% Called when a test case is skipped by either user action
%% or due to an init function failing.
on_tc_skip(_Suite, _TC, _Reason, State) ->
    State#state{skipped = State#state.skipped + 1}.

%% Called when the scope of the CTH is done.
terminate(State) ->
    %% use append to avoid data loss if the path is reused
    {ok, File} = file:open(State#state.filename, [write, append]),
    io:format(File, "~p.~n", [results(State)]),
    file:close(File),
    ok.

results(State) ->
    #state{skipped = Skipped, data = Data, total = Total} = State,
    #test_run{total = Total, skipped = Skipped, suites = lists:reverse(Data)}.

內建 CTH

Common Test 隨附了一些通用 CTH,使用者可以啟用這些 CTH 以提供通用測試功能。當 common_test 開始執行時,預設會啟用其中一些 CTH。可以透過在命令列或測試規格中將 enable_builtin_hooks 設定為 false 來停用它們。Common Test 隨附下列兩個 CTH:

  • cth_log_redirect - 內建

    捕獲所有通常由預設記錄器處理程式列印的日誌事件,並將它們列印到目前的測試案例日誌。如果事件無法與測試案例相關聯,則會將其列印在 Common Test 框架日誌中。當測試案例並行執行且事件發生在測試案例之間時,就會發生這種情況。

    日誌事件使用名為 cth_log_redirect 的 記錄器處理程式處理。格式和層級是從 cth 啟動時的目前 default 處理程式複製而來。如果您想使用其他層級,請在啟動 common_test 之前變更 default 處理程式層級,或使用 logger:set_handler_config/3 API。

    此鉤子支援以下選項:

    • {mode, add} - 將 cth_log_redirect 新增至預設記錄處理程式:日誌將透過預設處理程式發送到標準輸出,並發送到 Common Test HTML 日誌。這是預設行為。

    • {mode, replace} - 使用 cth_log_redirect 取代 default 記錄處理程式,而不是同時記錄到預設處理程式和此處理程式。這有效地靜音了在測試執行期間通常會列印到標準輸出的任何記錄器輸出。若要啟用此模式,您可以將下列選項傳遞給 ct_run

      -enable_builtin_hooks false -ct_hooks cth_log_redirect [{mode,replace}]

  • cth_surefire - 非內建

    捕獲所有測試結果並將其作為 surefire XML 輸出到檔案中。預設情況下,建立的檔案名為 junit_report.xml。可以透過設定此鉤子的選項 path 來變更檔案名稱,例如:

    -ct_hooks cth_surefire [{path,"/tmp/report.xml"}]

    如果設定了選項 url_base,則會為每個 testsuitetestcase XML 元素新增一個名為 url 的額外屬性。該值是根據 url_base 和測試套件或測試案例日誌的相對路徑建構而成的,例如:

    -ct_hooks cth_surefire [{url_base, "http://myserver.com/"}]

    產生類似於以下的 URL 屬性值:

    "http://myserver.com/ct_run.ct@myhost.2012-12-12_11.19.39/ x86_64-unknown-linux-gnu.my_test.logs/run.2012-12-12_11.19.39/suite.log.html"

    例如,Jenkins 可以使用 Surefire XML 來顯示測試結果。