檢視原始碼 測試案例與套件之間的相依性

一般

在建立測試套件時,強烈建議不要在測試案例之間建立相依性,也就是說,不要讓測試案例依賴先前測試案例的結果。這樣做有許多原因,例如:

  • 這會導致無法單獨執行測試案例。
  • 這會導致無法以不同的順序執行測試案例。
  • 這會使除錯變得困難(因為錯誤可能是另一個測試案例中的問題所造成的,而不是正在失敗的測試案例)。
  • 沒有良好且明確的方法來宣告相依性,因此在測試套件程式碼和測試記錄中很難看到和理解這些相依性。
  • 擴展、重組和維護具有測試案例相依性的測試套件是很困難的。

通常有足夠的方法可以繞過對測試案例相依性的需求。一般來說,問題與受測系統 (SUT) 的狀態有關。一個測試案例的動作可能會改變系統狀態。為了讓其他測試案例正常執行,必須知道這個新的狀態。

建議不要在測試案例之間傳遞資料,而是讓測試案例從 SUT 讀取狀態並執行斷言(也就是說,如果狀態如預期,則讓測試案例執行;否則重置或失敗)。也建議使用狀態來設定測試案例正常執行所需的變數。常見的動作通常可以實作為測試案例呼叫的程式庫函式,以便將 SUT 設定為所需的狀態。(如有必要,也可以單獨測試這些常見動作,以確保它們如預期般運作)。有時也可以(但並非總是理想的)將測試分組在一個測試案例中,也就是說,讓一個測試案例執行「情境」測試(由子測試組成的測試)。

例如,考慮一個受測的伺服器應用程式。以下功能將被測試:

  • 啟動伺服器
  • 設定伺服器
  • 將用戶端連線至伺服器
  • 從伺服器中斷用戶端連線
  • 停止伺服器

列出的功能之間存在明顯的相依性。如果伺服器尚未啟動,則無法設定伺服器;在伺服器正確設定之前,無法連線用戶端,依此類推。如果我們想要每個功能都有一個測試案例,我們可能會嘗試始終以宣告的順序執行測試案例,並在案例之間傳遞可能的資料(身分、控制代碼等等),因此在它們之間引入相依性。

為了避免這種情況,我們可以考慮為每個測試啟動和停止伺服器。因此,我們可以將啟動和停止動作實作為要從 init_per_testcaseend_per_testcase 呼叫的常用函式。(請記住單獨測試啟動和停止功能。)組態也可以實作為通用函式,或許可以與啟動函式分組。最後,可以將連線和中斷用戶端連線的測試分組到一個測試案例中。產生的套件可以如下所示:

-module(my_server_SUITE).
-compile(export_all).
-include_lib("ct.hrl").

%%% init and end functions...

suite() -> [{require,my_server_cfg}].

init_per_testcase(start_and_stop, Config) ->
    Config;

init_per_testcase(config, Config) ->
    [{server_pid,start_server()} | Config];

init_per_testcase(_, Config) ->
    ServerPid = start_server(),
    configure_server(),
    [{server_pid,ServerPid} | Config].

end_per_testcase(start_and_stop, _) ->
    ok;

end_per_testcase(_, Config) ->
    ServerPid = proplists:get_value(server_pid, Config),
    stop_server(ServerPid).

%%% test cases...

all() -> [start_and_stop, config, connect_and_disconnect].

%% test that starting and stopping works
start_and_stop(_) ->
    ServerPid = start_server(),
    stop_server(ServerPid).

%% configuration test
config(Config) ->
    ServerPid = proplists:get_value(server_pid, Config),
    configure_server(ServerPid).

%% test connecting and disconnecting client
connect_and_disconnect(Config) ->
    ServerPid = proplists:get_value(server_pid, Config),
    {ok,SessionId} = my_server:connect(ServerPid),
    ok = my_server:disconnect(ServerPid, SessionId).

%%% common functions...

start_server() ->
    {ok,ServerPid} = my_server:start(),
    ServerPid.

stop_server(ServerPid) ->
    ok = my_server:stop(),
    ok.

configure_server(ServerPid) ->
    ServerCfgData = ct:get_config(my_server_cfg),
    ok = my_server:configure(ServerPid, ServerCfgData),
    ok.

儲存組態資料

有時無法或不可行實作獨立的測試案例。也許無法讀取 SUT 狀態。也許無法重置 SUT,並且重新啟動系統需要太長時間。在需要測試案例相依性的情況下,CT 提供了一種結構化的方法,將資料從一個測試案例傳遞到下一個測試案例。相同的機制也可以用於將資料從一個測試套件傳遞到下一個測試套件。

傳遞資料的機制稱為 save_config。其概念是一個測試案例(或套件)可以儲存 Config 的目前值,或任何鍵值組的清單,以便下一個執行的測試案例(或測試套件)可以讀取它。組態資料不會永久儲存,而只能從一個案例(或套件)傳遞到下一個案例(或套件)。

若要儲存 Config 資料,請從 end_per_testcase 或主要測試案例函式傳回元組 {save_config,ConfigList}

若要讀取先前測試案例儲存的資料,請將 proplists:get_valuesaved_config 金鑰搭配使用,如下所示:

{Saver,ConfigList} = proplists:get_value(saved_config, Config)

Saver (atom/0) 是先前測試案例的名稱(資料儲存在此處)。proplists:get_value 函式也可用於從回溯的 ConfigList 中擷取特定資料。強烈建議 Saver 始終與儲存測試案例的預期名稱相符。這樣可以避免因測試套件重組而產生的問題。此外,這可以使相依性更明確,並使測試套件更易於讀取和維護。

若要將資料從一個測試套件傳遞到另一個測試套件,請使用相同的機制。資料將由函式 end_per_suite 儲存,並由後續套件中的函式 init_per_suite 讀取。當在套件之間傳遞資料時,Saver 會攜帶測試套件的名稱。

範例

-module(server_b_SUITE).
-compile(export_all).
-include_lib("ct.hrl").

%%% init and end functions...

init_per_suite(Config) ->
    %% read config saved by previous test suite
    {server_a_SUITE,OldConfig} = proplists:get_value(saved_config, Config),
    %% extract server identity (comes from server_a_SUITE)
    ServerId = proplists:get_value(server_id, OldConfig),
    SessionId = connect_to_server(ServerId),
    [{ids,{ServerId,SessionId}} | Config].

end_per_suite(Config) ->
    %% save config for server_c_SUITE (session_id and server_id)
    {save_config,Config}

%%% test cases...

all() -> [allocate, deallocate].

allocate(Config) ->
    {ServerId,SessionId} = proplists:get_value(ids, Config),
    {ok,Handle} = allocate_resource(ServerId, SessionId),
    %% save handle for deallocation test
    NewConfig = [{handle,Handle}],
    {save_config,NewConfig}.

deallocate(Config) ->
    {ServerId,SessionId} = proplists:get_value(ids, Config),
    {allocate,OldConfig} = proplists:get_value(saved_config, Config),
    Handle = proplists:get_value(handle, OldConfig),
    ok = deallocate_resource(ServerId, SessionId, Handle).

若要從要跳過的測試案例儲存 Config 資料,請傳回元組 {skip_and_save,Reason,ConfigList}

結果是測試案例會被跳過,並將 Reason 列印到記錄檔(如先前所述),並且將 ConfigList 儲存到下一個測試案例。ConfigList 可以使用 proplists:get_value(saved_config, Config) 讀取,如先前所述。skip_and_save 也可以從 init_per_suite 傳回。在此情況下,儲存的資料可以由後續套件中的 init_per_suite 讀取。

序列

有時測試案例彼此相依,因此如果一個案例失敗,則不會執行後續測試。通常,如果使用 save_config 功能,並且預期要儲存資料的測試案例當機,則下一個案例無法執行。Common Test 提供了一種宣告此類相依性的方法,稱為序列。

測試案例序列定義為具有 sequence 屬性的測試案例群組。測試案例群組透過測試套件中的函式 groups/0 定義(詳細資訊,請參閱章節 測試案例群組)。

例如,為了確保如果 server_b_SUITE 中的 allocate 當機,則會跳過 deallocate,可以定義以下序列:

groups() -> [{alloc_and_dealloc, [sequence], [alloc,dealloc]}].

假設套件包含測試案例 get_resource_status,該案例獨立於其他兩個案例,則函式 all 可以如下所示:

all() -> [{group,alloc_and_dealloc}, get_resource_status].

如果 alloc 成功,則也會執行 dealloc。但是,如果 alloc 失敗,則不會執行 dealloc,而是在 HTML 記錄中標記為 SKIPPED。無論 alloc_and_dealloc 案例發生什麼情況,都會執行 get_resource_status

序列中的測試案例會依序執行,直到全部成功或一個失敗。如果一個失敗,則會跳過序列中所有後續案例。在該點之前成功的序列中的案例會在記錄中報告為成功。可以指定任意數量的序列。

範例

groups() -> [{scenarioA, [sequence], [testA1, testA2]},
             {scenarioB, [sequence], [testB1, testB2, testB3]}].

all() -> [test1,
          test2,
          {group,scenarioA},
          test3,
          {group,scenarioB},
          test4].

序列群組可以有子群組。此類子群組可以具有任何屬性,也就是說,它們不一定也必須是序列。如果您希望子群組的狀態影響上面層次的序列,請從 end_per_group/2 傳回 {return_group_result,Status},如撰寫測試套件中的 重複群組 章節所述。失敗的子群組 (Status == failed) 會導致序列的執行失敗,與測試案例的方式相同。