13. Dependencies between Test Cases and Suites
13测试用例与套件之间的依赖关系
13.1一般
在创建测试套件时,强烈建议不要在测试用例之间创建依赖项,也就是说,让测试用例依赖于以前测试用例的结果。造成这种情况的原因有多种,例如:
- 这使得单独运行测试用例变得不可能。
- 这样就不可能以不同的顺序运行测试用例。
- 它使调试变得困难(因为在不同的测试案例中,问题可能会导致故障而不是某一个的故障)。
- 没有明确的方法来声明依赖关系,因此在测试套件代码和测试日志中很难看到和理解这些依赖关系。
- 扩展,重构和维护测试用例依赖关系的测试套件非常困难。
通常有足够的方法来解决测试用例依赖关系的需求。通常,问题与被测系统(SUT)的状态有关。一个测试用例的动作可以改变系统状态。为了让其他测试用例能够正常运行,必须知道这个新状态。
建议测试用例从SUT读取状态并执行断言(也就是说,如果状态符合预期,则运行测试用例,否则将重置或失败),而不是在测试用例之间传递数据。还建议使用状态来设置测试用例正确执行所需的变量。通常的操作通常可以作为测试用例的库函数来实现,以便调用将SUT设置为所需状态。(如果需要,这些常见行为也可以单独进行测试,以确保它们按预期工作)。在一个测试用例中将测试分组在一起,也就是让测试用例执行一个“场景”测试(一个由子测试组成的测试),有时也可能但并非总是可取的。
例如,考虑正在测试的服务器应用程序。将测试以下功能:
- 启动服务器
- 配置服务器
- 将客户端连接到服务器
- 断开客户端与服务器的连接
- 停止服务器
列出的功能之间有明显的依赖关系。如果服务器没有首先启动,则不能配置服务器,在服务器配置正确之前连接客户端,等等。如果我们希望每个函数都有一个测试用例,我们可能会试图总是按照规定的顺序运行测试用例,并在这些用例之间携带可能的数据(身份,句柄等),从而引入它们之间的依赖关系。
为了避免这种情况,我们可以考虑为每个测试启动和停止服务器。因此,我们可以将启动和停止操作实现为从init_per_testcase
和中调用的常用函数end_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(_, _) ->
ServerPid = ?config(server_pid),
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 = ?config(server_pid, Config),
configure_server(ServerPid).
%% test connecting and disconnecting client
connect_and_disconnect(Config) ->
ServerPid = ?config(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.
13.2保存配置数据
有时实施独立测试用例是不可能或不可行的。也许不可能读取SUT状态。也许重置SUT是不可能的,重启系统需要很长时间。在需要测试用例依赖的情况下,CT提供了一种结构化的方式来将数据从一个测试用例传送到下一个测试用例。也可以使用相同的机制将数据从一个测试套件传送到下一个测试套件。
传递数据的机制被调用save_config
。其思想是一个测试用例(或套件)可以保存当前的值Config
,或者任何键值元组的列表,以便下一个正在执行的测试用例(或测试套件)可以读取它。配置数据不会永久保存,但只能从一个案例(或套件)传递到下一个案例。
要保存Config
数据,返回元组{save_config,ConfigList}
从end_per_testcase
或从主测试用例功能。
要读取以前的测试用例保存的数据,请按如下所示使用宏config
和saved_config
键:
{Saver,ConfigList} = ?config(saved_config, Config)
Saver
(atom()
)是以前的测试用例的名称(数据保存的地方)。这个config
宏也可以用来提取召回的特定数据ConfigList
。强烈建议Saver
始终将其与保存测试用例的预期名称进行匹配。这样可以避免由于测试套件重组而导致的问题。此外,它使依赖更加明确,测试套件更易于阅读和维护。
为了将数据从一个测试套件传递到另一个测试套件,使用相同的机制。数据将通过finction进行保存,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} = ?config(saved_config, Config),
%% extract server identity (comes from server_a_SUITE)
ServerId = ?config(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} = ?config(ids, Config),
{ok,Handle} = allocate_resource(ServerId, SessionId),
%% save handle for deallocation test
NewConfig = [{handle,Handle}],
{save_config,NewConfig}.
deallocate(Config) ->
{ServerId,SessionId} = ?config(ids, Config),
{allocate,OldConfig} = ?config(saved_config, Config),
Handle = ?config(handle, OldConfig),
ok = deallocate_resource(ServerId, SessionId, Handle).
要保存Config
要跳过的测试用例的数据,请返回元组{skip_and_save,Reason,ConfigList}
。
结果是测试用例被跳过并Reason
打印到日志文件(如前所述)并ConfigList
保存到下一个测试用例中。ConfigList
可以使用?config(saved_config, Config)
,如前所述读取。skip_and_save
也可以退货init_per_suite
。在这种情况下,保存的数据可以init_per_suite
在随后的套件中读取。
13.3序列
有时候测试用例相互依赖,所以如果一个用例失败了,下面的测试不会被执行。通常情况下,如果使用该save_config
工具并且希望保存数据崩溃的测试用例,则以下情况将无法运行。Common Test
提供了一种声明这种依赖关系的方法,称为序列。
一系列测试用例被定义为一个具有sequence
属性的测试用例组。测试用例组是通过groups/0
测试套件中的函数定义的(详情请参见章节Test Case Groups
。
例如,要确保如果allocate
在server_b_SUITE
崩溃中deallocate
被跳过,可以定义以下序列:
groups() -> [{alloc_and_dealloc, [sequence], [alloc,dealloc]}].
假设套件包含测试用例get_resource_status
这是独立于其他两种情况,然后起作用。all
如下所示:
all() -> [{group,alloc_and_dealloc}, get_resource_status].
如果alloc
成功,dealloc
也会执行。如果alloc
失败,dealloc
则不会执行,而会标记为SKIPPED
在HTML日志中。get_resource_status
无论发生什么事情都会运行alloc_and_dealloc
。
序列中的测试用例按照顺序执行,直到所有的测试成功或失败。如果失败,则跳过序列中的所有情况。据报告,到那时为止,序列中成功的案例在日志中是成功的。可以指定任意数量的序列。
例子:
groups() -> [{scenarioA, [sequence], [testA1, testA2]},
{scenarioB, [sequence], [testB1, testB2, testB3]}].
all() -> [test1,
test2,
{group,scenarioA},
test3,
{group,scenarioB},
test4].
一个序列组可以有子组。这样的小组可以具有任何属性,也就是说,它们不需要也是序列。如果你想在亚组的状态,影响到一级序列上方,返回{return_group_result,Status}
的end_per_group/2
,如节中描述Repeated Groups
的写作测试套件。失败的子组(Status == failed
)导致序列的执行失败,与测试用例一样。