查看源代码 通用测试钩子

概述

通用测试钩子 (CTH) 框架允许在所有测试套件调用之前和之后使用钩子来扩展 Common Test 的默认行为。CTH 允许高级 Common Test 用户抽象出多个测试套件共有的行为,而无需在所有测试套件中添加库调用。这可以用于日志记录、启动和监视外部系统、构建测试所需的 C 文件等。

简而言之,CTH 允许你执行以下操作:

  • 在每次套件配置调用之前操作运行时配置。
  • 操作所有套件配置调用的返回值,并在扩展中操作测试本身的结果。

以下部分描述如何使用 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,当 Common Test 运行时,其 BEAM 文件必须在代码路径中可用。ct_run 接受 -pa 命令行选项。

覆盖 CTH

默认情况下,每次安装 CTH 都会导致其新实例被激活。如果你想在测试规范中覆盖 CTH,同时仍然在套件信息函数中保留它们,这可能会导致问题。id/1 回调的存在是为了解决这个问题。通过在两个位置返回相同的 idCommon Test 知道该 CTH 已经安装,并且不会尝试再次安装它。

CTH 执行顺序

默认情况下,每个已安装的 CTH 都按照安装顺序执行 init 调用,然后反向执行 end 调用。此顺序可以称为以测试为中心的顺序,因为在执行测试用例后顺序会反转,并且与 ct_hooks_order 选项的默认值 (test) 相对应。

基于安装的顺序并不总是理想的,因此 Common Test 允许用户为每个钩子指定优先级。优先级可以在 CTH 函数 init/2 中指定,也可以在安装钩子时指定。在安装时指定的优先级将覆盖 CTH 返回的优先级。

在某些情况下,不希望所有 end 调用都反向排序,而是用户可能更喜欢 post 钩子调用反向排序。可以使用 ct_hooks_order 选项和 config 值启用此行为。启用此选项后,执行顺序以配置为中心,因为反向排序发生在每个配置函数之后,而不是与测试用例相关。

请注意,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/0pre_init_per_suite/3 被调用post_end_per_suite/4 已针对该测试套件调用
init_per_suite/1post_init_per_suite/4 被调用post_end_per_suite/4 已针对该测试套件调用
init_per_group/2post_init_per_group/5 被调用post_end_per_group/5 已针对该组调用

表:CTH 的范围

CTH 进程和表

CTH 的运行进程作用域与普通测试套件相同,即不同的进程执行 init_per_suite 钩子,然后执行 init_per_groupper_testcase 钩子。因此,如果你想在 CTH 中生成一个进程,你不能链接到 CTH 进程,因为它在 post 钩子结束后退出。此外,如果你出于某种原因需要一个带有 CTH 的 ETS 表,你必须生成一个处理它的进程。

外部配置数据和日志记录

可以通过调用 ct:get_config/1,2,3 读取 CTH 中的配置数据值(如 请求和读取配置数据 部分中所述)。与往常一样,有问题的配置变量必须首先由套件、组或测试用例信息函数或函数 ct:require/1/2 请求。后者也可以在 CT 钩子函数中使用。

CT 钩子函数可以调用 ct 接口中的任何日志记录函数,以将信息打印到日志文件,或在套件概览页面中添加注释。

操作测试

通过 CTH,可以操作测试和配置函数的结果。使用 CTH 执行此操作的主要目的是允许从测试套件中抽象出通用模式,并将其应用于多个测试套件,而无需复制任何代码。CTH 的所有回调函数都遵循以下描述的通用接口。

Common Test 始终调用所有可用的钩子函数,甚至是套件中未实现的配置函数的前置和后置钩子。例如,即使测试套件 x_SUITE 不导出 init_per_suite/1,也会为测试套件 x_SUITE 调用 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 在最后执行正确的操作。

后置钩子

在 CTH 中,可以在以下函数之后挂钩行为:

这是在名为 post_<函数名称> 的 CTH 函数中完成的。这些函数采用参数 SuiteNameName(组或测试用例名称,如果适用)、ConfigReturnCTHState。在这种情况下,Config 与调用测试用例的 Config 相同。Return 是测试用例返回的值。如果测试用例因崩溃而失败,则 Return{'EXIT',{{Error,Reason},Stacktrace}}

CTH 函数的返回值始终是套件/组/测试的结果和更新后的 CTHState 的组合。如果你不希望回调影响测试的结果,请按 CTH 中给出的方式返回 Return 数据。你还可以修改测试结果。通过返回删除了元素 tc_statusConfig 列表,你可以从测试失败中恢复。与所有前置钩子一样,也可以在后置钩子中使测试用例失败/跳过。

示例

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 或 termination 阶段生成的任何系统错误或进度报告都保存在测试前和测试后 I/O 日志中。(对于使用 ct:log/2ct:pal/2 进行的任何打印输出也是如此)。

为了确保 Common Test 不在外部应用程序准备好之前开始执行测试,或者关闭其日志文件并关闭,Common Test 可以与应用程序同步。在启动和关闭期间,Common Test 可以通过让 CTH 在 init 或 terminate 函数中评估 receive 表达式来暂停。?CT_HOOK_INIT_PROCESS (执行钩子 init 函数的进程) 和 ?CT_HOOK_TERMINATE_PROCESS (执行钩子 terminate 函数的进程) 这两个宏分别指定要向其发送消息的正确的 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 来禁用它们。 以下两个 CTH 随 Common Test 一起提供

  • cth_log_redirect - 内置

    捕获所有通常由默认记录器处理程序打印的日志事件,并将它们打印到当前测试用例日志。如果事件无法与测试用例关联,则会在 Common Test 框架日志中打印。这发生在并行运行的测试用例以及测试用例之间发生的事件中。

    日志事件使用名为 cth_log_redirect 的 Logger 处理程序处理。当 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/[email protected]_11.19.39/ x86_64-unknown-linux-gnu.my_test.logs/run.2012-12-12_11.19.39/suite.log.html"

    例如,Jenkins 可以使用 Surefire XML 来显示测试结果。