查看源代码 测试用例和套件之间的依赖关系

概述

在创建测试套件时,强烈建议不要在测试用例之间创建依赖关系,即不要让测试用例依赖于先前测试用例的结果。这样做有很多原因,例如:

  • 这使得无法单独运行测试用例。
  • 这使得无法以不同的顺序运行测试用例。
  • 这使得调试变得困难(因为故障可能是由与失败的测试用例不同的测试用例中的问题引起的)。
  • 没有好的、明确的方法来声明依赖关系,因此在测试套件代码和测试日志中可能很难看到和理解这些依赖关系。
  • 扩展、重组和维护具有测试用例依赖关系的测试套件很困难。

通常有足够的手段来解决对测试用例依赖性的需求。通常,问题与被测系统 (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 将为下一个测试用例保存。可以使用 proplists:get_value(saved_config, Config) 读取 ConfigList,如前所述。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 日志中标记为 SKIPPEDget_resource_status 的运行不受 alloc_and_dealloc 用例的影响。

序列中的测试用例按顺序执行,直到全部成功或一个失败。如果一个失败,则序列中所有后续用例都会被跳过。到那时为止成功的序列中的用例将在日志中报告为成功。可以指定任意数量的序列。

示例

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) 会导致序列执行失败,就像测试用例失败一样。