查看源码 编写测试套件

对测试套件作者的支持

ct 模块为编写测试用例提供了主要接口。这包括例如以下内容:

  • 用于打印和记录的函数
  • 用于读取配置数据的函数
  • 用于以错误原因终止测试用例的函数
  • 用于向 HTML 概述页面添加注释的函数

有关这些函数的详细信息,请参阅模块 ct

Common Test 应用程序还包括其他名为 ct_<组件> 的模块,这些模块提供各种支持,主要是简化通信协议(如 RPC、SNMP、FTP、Telnet 等)的使用。

测试套件

测试套件是一个普通的 Erlang 模块,其中包含测试用例。建议模块的名称采用 *_SUITE.erl 的形式。否则,Common Test 中的目录和自动编译功能将无法找到它(至少默认情况下不能)。

还建议在所有测试套件模块中包含 ct.hrl 头文件。

每个测试套件模块都必须导出函数 all/0,该函数返回要在该模块中执行的所有测试用例组和测试用例的列表。

测试套件要实现的的回调函数都列在模块 ct_suite 中。 它们也会在本用户指南的后续部分中详细描述。

每个套件的初始化和结束

每个测试套件模块都可以包含可选的配置函数 init_per_suite/1end_per_suite/1。 如果定义了初始化函数,则必须定义结束函数。

如果存在 init_per_suite,则在执行测试用例之前会首先调用它。 它通常包含套件中所有测试用例共用的初始化,这些初始化只需执行一次。init_per_suite 建议用于在被测系统 (SUT) 或 Common Test 主机节点(或两者)上设置和验证状态和环境,以便套件中的测试用例正确执行。以下是初始配置操作的示例:

  • 打开到 SUT 的连接
  • 初始化数据库
  • 运行安装脚本

end_per_suite 在测试套件执行的最后阶段(在最后一个测试用例完成后)调用。 该函数旨在用于 init_per_suite 之后的清理。

init_per_suiteend_per_suite 与测试用例一样,在专用的 Erlang 进程上执行。 但是,这些函数的结果不包括在成功、失败和跳过案例的测试运行统计信息中。

init_per_suite 的参数是 Config,即每个测试用例都作为输入参数的运行时配置数据的相同键值列表。init_per_suite 可以使用测试用例所需的信息修改此参数。 可能修改的 Config 列表是该函数的返回值。

如果 init_per_suite 失败,则测试套件中的所有测试用例都会自动跳过(所谓的自动跳过),包括 end_per_suite

请注意,如果套件中不存在 init_per_suiteend_per_suite,则 Common Test 会改为调用虚拟函数(具有相同的名称),以便可以将 hook 函数生成的输出保存到这些虚拟函数的日志文件中。 有关详细信息,请参阅 Common Test Hooks

每个测试用例的初始化和结束

每个测试套件模块都可以包含可选的配置函数 init_per_testcase/2end_per_testcase/2。 如果定义了初始化函数,则必须定义结束函数。

如果存在 init_per_testcase,则在套件中的每个测试用例之前都会调用它。 它通常包含必须为每个测试用例完成的初始化(类似于套件的 init_per_suite)。

end_per_testcase/2 在每个测试用例完成后调用,以便在 init_per_testcase 之后进行清理。

注意

但是,如果 end_per_testcase 崩溃,则测试结果不受影响。 同时,此事件会在测试执行日志中报告。

这些函数的第一个参数是测试用例的名称。 此值可以与函数子句或条件表达式中的模式匹配一起使用,以便为不同的测试用例选择不同的初始化和清理例程,或为许多或所有测试用例执行相同的例程。

第二个参数是 Config 运行时配置数据的键值列表,该列表的值与 init_per_suite 返回的列表相同。init_per_testcase/2 可以修改此参数或按原样返回它。init_per_testcase/2 的返回值作为参数 Config 传递给测试用例本身。

测试服务器将忽略 end_per_testcase/2 的返回值,除了 save_configfail 元组。

end_per_testcase 可以检查测试用例是否成功。(这反过来可以确定如何执行清理)。这是通过从 Config 中读取标记为 tc_status 的值来完成的。该值是以下值之一:

  • ok

  • {failed,Reason}

    其中 Reasontimetrap_timeout,来自 exit/1 的信息,或运行时错误的详细信息

  • {skipped,Reason}

    其中 Reason 是用户特定的术语

如果测试用例因调用 ct:abort_current_testcase/1 或在 timetrap 超时后终止,则甚至会调用函数 end_per_testcase/2。 但是,end_per_testcase 随后在与测试用例函数不同的进程上执行。 在这种情况下,end_per_testcase 不能通过返回 {fail,Reason} 或使用 {save_config,Data} 保存数据来更改测试用例终止的原因。

在以下两种情况下,会跳过测试用例:

  • 如果 init_per_testcase 崩溃(称为自动跳过)。
  • 如果 init_per_testcase 返回元组 {skip,Reason}(称为用户跳过)。

也可以通过从 init_per_testcase 返回元组 {fail,Reason} 来将测试用例标记为失败而不执行它。

注意

如果 init_per_testcase 崩溃或返回 {skip,Reason}{fail,Reason},则不会调用函数 end_per_testcase

如果在执行 end_per_testcase 期间确定要将成功测试用例的状态更改为失败,则 end_per_testcase 可以返回元组 {fail,Reason}(其中 Reason 描述测试用例失败的原因)。

由于 init_per_testcaseend_per_testcase 与测试用例在同一个 Erlang 进程上执行,因此来自这些配置函数的输出会包含在测试用例日志文件中。

测试用例

测试服务器关心的最小单位是测试用例。每个测试用例可以测试很多东西,例如,使用不同的参数多次调用相同的接口函数。

作者可以选择在每个测试用例中放置许多或少量测试。下面是一些需要记住的事情:

  • 许多小型测试用例往往会导致额外的且可能是重复的代码,以及由于初始化和清理的开销较大而导致测试执行缓慢。避免重复代码,例如,通过使用公共的帮助函数。否则,生成的套件会变得难以阅读和理解,并且维护成本很高。
  • 较大的测试用例如果失败,则更难判断哪里出了问题。此外,当发生错误时,可能会跳过大部分测试代码。
  • 当测试用例变得太大和广泛时,可读性和可维护性会受到影响。不能确定生成的日志文件是否很好地反映了执行的测试数量。

测试用例函数接受一个参数 Config,其中包含配置信息,例如 data_dirpriv_dir。(有关这些的详细信息,请参阅 数据和私有目录 部分。)调用时 Config 的值与前面提到的 init_per_testcase 的返回值相同。

注意

测试用例函数参数 Config 不应与可以从配置文件中检索的信息(使用 ct:get_config/1/2)混淆。 测试用例参数 Config 用于测试套件和测试用例的运行时配置,而配置文件则用于包含与 SUT 相关的数据。 这两种类型的配置数据处理方式不同。

由于参数 Config 是键值对元组的列表,即一种名为属性列表的数据类型,因此可以使用 proplists 模块进行处理。例如,可以使用函数 proplists:get_value/2 查找并返回一个值。此外,或者作为替代方案,通用的 lists 模块也包含有用的函数。通常,对 Config 执行的唯一操作是插入(将元组添加到列表的头部)和查找。要查找配置中的值,可以使用 proplists:get_value。例如:PrivDir = proplists:get_value(priv_dir, Config)

可以通过多种方式自定义测试用例结果。有关详细信息,请参阅 Module:Testcase/1 手册中的 ct_suite 模块。

测试用例信息函数

对于每个测试用例函数,可以有一个额外的函数,其名称相同但不带参数。这就是测试用例信息函数。它应该返回一个带标签的元组列表,该列表指定有关测试用例的各种属性。

以下标签具有特殊含义

  • timetrap - 设置允许测试用例执行的最大时间。如果超出此时间,则测试用例将失败,原因 timetrap_timeout。请注意,init_per_testcaseend_per_testcase 包含在 timetrap 时间中。有关详细信息,请参阅 Timetrap 超时 部分。

  • userdata - 指定与测试用例相关的任何数据。可以使用 ct:userdata/3 实用程序函数随时检索此数据。

  • silent_connections - 有关详细信息,请参阅 静默连接 部分。

  • require - 指定测试用例所需的配置变量。如果在任何测试系统配置文件中都找不到所需的配置变量,则会跳过测试用例。

    如果变量在任何配置文件中都找不到,也可以为所需的变量指定一个默认值。要指定默认值,请将 {default_config,ConfigVariableName,Value} 形式的元组添加到测试用例信息列表(列表中的位置无关紧要)。

    示例

    testcase1() ->
        [{require, ftp},
         {default_config, ftp, [{ftp, "my_ftp_host"},
                                {username, "aladdin"},
                                {password, "sesame"}]}}].
    testcase2() ->
        [{require, unix_telnet, unix},
         {require, {unix, [telnet, username, password]}},
         {default_config, unix, [{telnet, "my_telnet_host"},
                                 {username, "aladdin"},
                                 {password, "sesame"}]}}].

有关 require 的更多信息,请参阅外部配置数据部分中的 请求和读取配置数据 部分和函数 ct:require/1/2

注意

为所需的变量指定默认值可能会导致测试用例始终执行。这可能不是期望的行为。

如果未针对特定测试用例专门设置 timetraprequire,或者两者都没有设置,则使用函数 suite/0 指定的默认值。

测试服务器会忽略除了前面提到的标签之外的其他标签。

以下是测试用例信息函数的示例

reboot_node() ->
    [
     {timetrap,{seconds,60}},
     {require,interfaces},
     {userdata,
         [{description,"System Upgrade: RpuAddition Normal RebootNode"},
          {fts,"http://someserver.ericsson.se/test_doc4711.pdf"}]}
    ].

测试套件信息函数

例如,可以在测试套件模块中使用函数 suite/0 设置默认的 timetrap 值,以及 require 外部配置数据。如果测试用例或组信息函数也指定了任何信息标签,则它将覆盖 suite/0 设置的默认值。有关详细信息,请参阅 测试用例信息函数测试用例组

以下选项也可以在套件信息列表中指定

以下是套件信息函数的示例

suite() ->
    [
     {timetrap,{minutes,10}},
     {require,global_names},
     {userdata,[{info,"This suite tests database transactions."}]},
     {silent_connections,[telnet]},
     {stylesheet,"db_testing.css"}
    ].

测试用例组

测试用例组是一组共享配置函数和执行属性的测试用例。测试用例组由函数 groups/0 定义,该函数应返回具有以下语法的术语

groups() -> GroupDefs

Types:

GroupDefs = [GroupDef]
GroupDef = {GroupName,Properties,GroupsAndTestCases}
GroupName = atom()
GroupsAndTestCases = [GroupDef | {group,GroupName} | TestCase |
                     {testcase,TestCase,TCRepeatProps}]
TestCase = atom()
TCRepeatProps = [{repeat,N} | {repeat_until_ok,N} | {repeat_until_fail,N}]

GroupName 是组的名称,并且在测试套件模块中必须唯一。可以通过在另一个组的 GroupsAndTestCases 列表中包含组定义来嵌套组。Properties 是该组的执行属性列表。可能的值如下所示

Properties = [parallel | sequence | Shuffle | {GroupRepeatType,N}]
Shuffle = shuffle | {shuffle,Seed}
Seed = {integer(),integer(),integer()}
GroupRepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
                  repeat_until_any_ok | repeat_until_any_fail
N = integer() | forever

解释

  • parallel - Common Test 并行执行该组中的所有测试用例。

  • sequence - 这些用例按照测试用例和套件之间的依赖关系部分中的 序列 部分所述的顺序执行。

  • shuffle - 该组中的用例以随机顺序执行。

  • repeat, repeat_until_* - 指示 Common Test 重复执行该组中的所有用例指定的次数,或者直到任何或所有用例失败或成功为止。

示例

groups() -> [{group1, [parallel], [test1a,test1b]},
             {group2, [shuffle,sequence], [test2a,test2b,test2c]}].

要指定组的执行顺序(也包括与任何组无关的测试用例),请将 {group,GroupName} 形式的元组添加到 all/0 列表中。

示例

all() -> [testcase1, {group,group1}, {testcase,testcase2,[{repeat,10}]}, {group,group2}].

也可以指定 all/0 中带有组元组的执行属性:{group,GroupName,Properties}。这些属性将覆盖组定义中指定的属性(请参阅前面的 groups/0)。这样,就可以运行相同的测试集,但使用不同的属性,而无需复制相关的组定义。

如果一个组包含子组,则也可以在组元组中指定这些子组的执行属性:{group,GroupName,Properties,SubGroups} 其中,SubGroups 是一个元组列表,{GroupName,Properties}{GroupName,Properties,SubGroups} 表示子组。在 groups/0 中为组定义的任何子组,如果未在 SubGroups 列表中指定,则将使用其预定义的属性执行。

示例

groups() -> [{tests1, [], [{tests2, [], [t2a,t2b]},
                          {tests3, [], [t31,t3b]}]}].

要执行组 tests1 两次,每次对 tests2 使用不同的属性

all() ->
   [{group, tests1, default, [{tests2, [parallel]}]},
    {group, tests1, default, [{tests2, [shuffle,{repeat,10}]}]}].

这等效于以下规范

all() ->
   [{group, tests1, default, [{tests2, [parallel]},
                              {tests3, default}]},
    {group, tests1, default, [{tests2, [shuffle,{repeat,10}]},
                              {tests3, default}]}].

default 表示将使用预定义的属性。

以下示例显示了如何在深度嵌套的组的场景中覆盖属性

groups() ->
   [{tests1, [], [{group, tests2}]},
    {tests2, [], [{group, tests3}]},
    {tests3, [{repeat,2}], [t3a,t3b,t3c]}].

all() ->
   [{group, tests1, default,
     [{tests2, default,
       [{tests3, [parallel,{repeat,100}]}]}]}].

为了便于阅读,所有语法定义都可以替换为函数调用,该函数的返回值应与预期的语法情况匹配。

示例

all() ->
   [{group, tests1, default, test_cases()},
    {group, tests1, default, [shuffle_test(),
                              {tests3, default}]}].
test_cases() ->
   [{tests2, [parallel]}, {tests3, default}].

shuffle_test() ->
   {tests2, [shuffle,{repeat,10}]}.

所描述的语法也可以在测试规范中使用,以在执行时更改组属性,而无需编辑测试套件。有关详细信息,请参阅运行测试和分析结果部分中的 测试规范 部分。

如图所示,可以组合属性。例如,如果同时指定 shufflerepeat_until_any_failsequence,则组中的测试用例将重复执行,并以随机顺序执行,直到某个测试用例失败。然后立即停止执行并跳过其余用例。

在组执行开始之前,会调用配置函数 init_per_group(GroupName, Config)。此函数返回的元组列表将以通常的方式通过参数 Config 传递给测试用例。init_per_group/2 用于对组中的测试用例进行通用初始化。在该组执行完成后,会调用函数 end_per_group(GroupName, Config)。此函数用于在 init_per_group/2 之后进行清理。如果定义了初始化函数,则也必须定义结束函数。

每当执行组时,如果套件中不存在 init_per_groupend_per_group,则 Common Test 会调用虚拟函数(名称相同)来代替。钩子函数生成的输出将保存到这些虚拟函数的日志文件中。有关详细信息,请参阅 Common Test 钩子部分中的 操作测试 部分。

注意

无论用例是否属于某个组,都会始终为每个单独的测试用例调用 init_per_testcase/2end_per_testcase/2

组的属性始终打印在 init_per_group/2 的 HTML 日志的顶部。组的总执行时间包含在 end_per_group/2 的日志的底部。

测试用例组可以嵌套,因此可以使用相同的 init_per_group/2end_per_group/2 函数来配置多组。可以通过在另一个组的测试用例列表中包含组定义或组名称引用来定义嵌套组。

示例

groups() -> [{group1, [shuffle], [test1a,
                                  {group2, [], [test2a,test2b]},
                                  test1b]},
             {group3, [], [{group,group4},
                           {group,group5}]},
             {group4, [parallel], [test4a,test4b]},
             {group5, [sequence], [test5a,test5b,test5c]}].

在前面的示例中,如果 all/0 按顺序 [{group,group1},{group,group3}] 返回组名称引用,则配置函数和测试用例的顺序变为以下内容(请注意,init_per_testcase/2end_per_testcase/2: 也始终被调用,但为了简化,在此示例中未包括)

init_per_group(group1, Config) -> Config1  (*)
     test1a(Config1)
     init_per_group(group2, Config1) -> Config2
          test2a(Config2), test2b(Config2)
     end_per_group(group2, Config2)
     test1b(Config1)
end_per_group(group1, Config1)
init_per_group(group3, Config) -> Config3
     init_per_group(group4, Config3) -> Config4
          test4a(Config4), test4b(Config4)  (**)
     end_per_group(group4, Config4)
     init_per_group(group5, Config3) -> Config5
          test5a(Config5), test5b(Config5), test5c(Config5)
     end_per_group(group5, Config5)
end_per_group(group3, Config3)

(*) 测试用例 test1atest1bgroup2 的顺序未定义,因为 group1 具有 shuffle 属性。

(**) 这些用例不是按顺序执行的,而是并行执行的。

属性不会从顶级组继承到嵌套子组。例如,在前面的示例中,group2 中的测试用例不是以随机顺序执行的(这是 group1 的属性)。

并行属性和嵌套组

如果组具有 parallel 属性,则其测试用例会同时生成并并行执行。但是,不允许测试用例与 end_per_group/2 并行执行,这意味着执行并行组的时间等于该组中最慢的测试用例的执行时间。并行运行测试用例的一个负面影响是,在组的函数 end_per_group/2 完成之前,HTML 摘要页面不会更新为指向各个测试用例日志的链接。

一个嵌套在并行组下的组开始与之前的(并行)测试用例并行执行(无论嵌套组具有什么属性)。然而,由于测试用例永远不会与同一组的 init_per_group/2end_per_group/2 并行执行,因此只有在嵌套组完成后,前一个组中剩余的并行用例才会生成。

并行测试用例和 I/O

一个并行测试用例将其组领导者作为私有 I/O 服务器。(有关组领导者概念的描述,请参阅 ERTS)。中央 I/O 服务器进程(处理常规测试用例和配置函数的输出)在执行并行组期间不响应 I/O 消息。理解这一点很重要,以避免某些陷阱,例如以下情况:

如果在执行过程中生成了一个进程 P,例如,在 init_per_suite/1 中,它会继承 init_per_suite 进程的组领导者。此组领导者是前面提到的中央 I/O 服务器进程。如果在稍后的时间,在并行测试用例执行期间,某个事件触发进程 P 调用 io:format/1/2,则该调用永远不会返回(因为组领导者处于无响应状态),并导致 P 挂起。

重复组

测试用例组可以重复执行一定次数(由整数指定)或无限次(由 forever 指定)。如果任何或所有用例失败或成功,也可以过早停止重复,也就是说,如果使用了任何属性 repeat_until_any_failrepeat_until_any_okrepeat_until_all_failrepeat_until_all_ok。如果使用基本的 repeat 属性,则测试用例的状态与重复操作无关。

子组的状态可以返回 (okfailed),以影响上一级组的执行。这是通过在 end_per_group/2 中查找 Config 列表中的 tc_group_properties 的值并检查组中测试用例的结果来实现的。如果要从组返回状态 failed 作为结果,则 end_per_group/2 应返回 {return_group_result,failed} 值。当评估是否要重复执行组时,Common Test 会考虑子组的状态(除非使用基本的 repeat 属性)。

tc_group_properties 的值是一个状态元组列表,每个元组都有键 okskippedfailed。状态元组的值是一个列表,其中包含已执行且结果具有相应状态的测试用例的名称。

以下是如何从组返回状态的示例

end_per_group(_Group, Config) ->
    Status = proplists:get_value(tc_group_result, Config),
    case proplists:get_value(failed, Status) of
        [] ->                                   % no failed cases
            {return_group_result,ok};
        _Failed ->                              % one or more failed
            {return_group_result,failed}
    end.

end_per_group/2 中也可以检查子组的状态(可能为了确定当前组要返回的状态)。这与前面的示例中所示的一样简单,只是组名存储在元组 {group_result,GroupName} 中,可以在状态列表中搜索该元组。

示例

end_per_group(group1, Config) ->
    Status = proplists:get_value(tc_group_result, Config),
    Failed = proplists:get_value(failed, Status),
    case lists:member({group_result,group2}, Failed) of
          true ->
              {return_group_result,failed};
          false ->
              {return_group_result,ok}
    end;
...

注意

当测试用例组重复时,配置函数 init_per_group/2end_per_group/2 也始终会在每次重复时被调用。

打乱的测试用例顺序

正常情况下,组中测试用例的执行顺序与组定义中测试用例列表指定的顺序相同。但是,如果设置了属性 shuffle,则 Common Test 会改为以随机顺序执行测试用例。

您可以为 shuffle 属性提供一个种子值(一个由三个整数组成的元组){shuffle,Seed}。这样,每次执行组时都可以创建相同的打乱顺序。如果未指定种子值,则 Common Test 会为打乱操作创建一个“随机”种子(使用 erlang:timestamp/0 的返回值)。种子值始终会打印到 init_per_group/2 日志文件中,以便可以在后续测试运行中用于重新创建相同的执行顺序。

注意

如果重复执行打乱的测试用例组,则种子不会在回合之间重置。

如果在具有 shuffle 属性的组中指定了子组,则此子组相对于组中测试用例(和其他子组)的执行顺序是随机的。但是,子组中测试用例的顺序不是随机的(除非子组具有 shuffle 属性)。

组信息函数

测试用例组信息函数 group(GroupName) 的用途与前面描述的套件和测试用例信息函数相同。但是,组信息函数的作用域是该组(GroupName)中的所有测试用例和子组。

示例

group(connection_tests) ->
   [{require,login_data},
    {timetrap,1000}].

组信息属性会覆盖使用套件信息函数设置的属性,并且可以反过来被测试用例信息属性覆盖。有关有效的信息属性和更多常规信息,请参阅测试用例信息函数

Init 和 End 配置的信息函数

信息函数也可以用于函数 init_per_suiteend_per_suiteinit_per_groupend_per_group,它们的工作方式与测试用例信息函数相同。例如,这对于设置 timetraps 和要求仅与相关配置函数相关的外部配置数据非常有用(而不影响为套件中的组和测试用例设置的属性)。

信息函数 init/end_per_suite()init/end_per_suite(Config) 调用,信息函数 init/end_per_group(GroupName)init/end_per_group(GroupName,Config) 调用。但是,信息函数不能与 init/end_per_testcase(TestCase, Config) 一起使用,因为这些配置函数在测试用例进程上执行并使用与测试用例相同的属性(即,测试用例信息函数 TestCase() 设置的属性)。有关有效的信息属性和更多常规信息,请参阅测试用例信息函数

数据和私有目录

在数据目录 data_dir 中,测试模块具有其测试所需的文件。data_dir 的名称是测试套件的名称,后跟 "_data"。例如,"some_path/foo_SUITE.beam" 的数据目录为 "some_path/foo_SUITE_data/"。为了提高可移植性,请使用此目录,也就是说,避免在套件中硬编码目录名称。由于数据目录与您的测试套件存储在同一个目录中,因此您可以依赖其在运行时的存在,即使您的测试套件目录的路径在测试套件实现和执行之间发生了更改。

priv_dir 是测试用例的私有目录。只要测试用例(或配置函数)需要将某些内容写入文件,就可以使用此目录。私有目录的名称由 Common Test 生成,Common Test 还会创建该目录。

默认情况下,Common Test 为每次测试运行创建一个中央私有目录,供所有测试用例共享。这并不总是合适的。尤其是当同一测试用例在测试运行期间多次执行时(也就是说,如果它们属于具有属性 repeat 的测试用例组),并且存在私有目录中的文件被覆盖的风险。在这些情况下,可以将 Common Test 配置为改为为每个测试用例和执行创建一个专用的私有目录。这是通过标志/选项 create_priv_dir 来完成的(与 ct_run 程序、ct:run_test/1 函数或作为测试规范项一起使用)。此选项有以下三个可能的值:

  • auto_per_run
  • auto_per_tc
  • manual_per_tc

第一个值表示默认的 priv_dir 行为,即每次测试运行创建一个私有目录。后两个值告诉 Common Test 为每个测试用例和执行生成一个唯一的测试目录名称。如果使用自动版本,则会自动创建所有私有目录。对于具有许多测试用例或重复的测试运行,或者两者都有的测试运行,这可能会非常低效。因此,如果改为使用手动版本,则测试用例必须在需要时告诉 Common Test 创建 priv_dir。它通过调用函数 ct:make_priv_dir/0 来完成此操作。

注意

不要依赖当前工作目录来读取和写入数据文件,因为这是不可移植的。所有临时文件都应写入 priv_dir,所有数据文件都应位于 data_dir 中。此外,Common Test 服务器会在每个用例开始时将当前工作目录设置为测试用例日志目录。

执行环境

每个测试用例都由一个专用的 Erlang 进程执行。该进程在测试用例开始时生成,并在测试用例完成时终止。配置函数 init_per_testcaseend_per_testcase 在与测试用例相同的进程上执行。

配置函数 init_per_suiteend_per_suite 与测试用例一样,在专用的 Erlang 进程上执行。

Timetrap 超时

测试用例的默认时间限制为 30 分钟,除非通过套件、组或测试用例信息函数指定了 timetrap。由 suite/0 定义的 timetrap 超时值是套件中每个测试用例(以及配置函数 init_per_suite/1end_per_suite/1init_per_group/2end_per_group/2)使用的值。由 group(GroupName) 定义的 timetrap 值会覆盖由 suite() 定义的值,并用于组 GroupName 及其任何子组中的每个测试用例。如果子组的 group/1 定义了 timetrap 值,它会覆盖其更高级别组的值。由单个测试用例(通过测试用例信息函数)设置的 timetrap 值会覆盖组级别和套件级别的 timetrap。

还可以在测试用例或配置函数的执行期间动态设置或重置 timetrap。这通过调用 ct:timetrap/1 来完成。此函数会取消当前的 timetrap,并启动一个新的 timetrap(该 timetrap 会一直保持活动状态,直到超时或当前函数结束)。

可以使用在启动时通过选项 multiply_timetraps 指定的乘数值来扩展 timetrap 值。也可以让测试服务器决定自动按比例放大 timetrap 超时值。也就是说,如果在测试期间运行诸如 covertrace 之类的工具。此功能默认禁用,可以使用启动选项 scale_timetraps 启用。

如果测试用例需要暂停一段时间,该时间也会乘以 multiply_timetraps(如果启用了 scale_timetraps,还可能会按比例放大),则可以使用函数 ct:sleep/1(而不是例如 timer:sleep/1)。

可以在套件、组和测试用例信息函数中,以及作为函数 ct:timetrap/1 的参数,将函数(fun/0{Mod,Func,Args} (MFA) 元组)指定为 timetrap 值。

示例

{timetrap,{my_test_utils,timetrap,[?MODULE,system_start]}}

ct:timetrap(fun() -> my_timetrap(TestCaseName, Config) end)

用户 timetrap 函数可以用于以下两件事

  • 充当 timetrap。当函数返回时,会触发超时。
  • 返回 timetrap 时间值(不是函数)。

在执行 timetrap 函数(在并行的专用 timetrap 进程上执行)之前,Common Test 会取消为测试用例或配置函数设置的任何先前计时器。当 timetrap 函数返回时,会触发超时,除非返回值是有效的 timetrap 时间,例如整数或 {SecMinOrHourTag,Time} 元组(有关详细信息,请参阅模块 ct_suite)。如果返回时间值,则会启动一个新的 timetrap,以便在指定时间后生成超时。

用户 timetrap 函数可以在延迟后返回时间值。则有效的 timetrap 时间是延迟时间加上返回的时间。

日志记录 - 类别和详细级别

Common Test 提供了以下三个用于打印字符串的主要函数

  • ct:log(Category, Importance, Format, FormatArgs, Opts)
  • ct:print(Category, Importance, Format, FormatArgs)
  • ct:pal(Category, Importance, Format, FormatArgs)

log/1,2,3,4,5 函数将字符串打印到测试用例日志文件中。print/1,2,3,4 函数将字符串打印到屏幕上。pal/1,2,3,4 函数将相同的字符串同时打印到文件和屏幕上。这些函数在模块 ct 中进行了描述。

可选的 Category 参数可以用于对日志输出进行分类。类别可用于以下两件事

  • 将输出的重要性与特定的详细级别进行比较。
  • 根据用户特定的 HTML 样式表 (CSS) 格式化输出。

参数 Importance 指定一个重要性级别,该级别与详细级别(常规和/或按类别设置)进行比较,以确定是否要显示输出。Importance 是范围 0..99 内的任何整数。ct.hrl 头文件中存在预定义的常量。默认重要性级别,?STD_IMPORTANCE(如果未提供参数 Importance,则使用)为 50。这也是用于标准 I/O 的重要性,例如,从使用 io:format/2io:put_chars/1 等进行的输出。

Importanceverbosity 启动标志/选项设置的详细级别进行比较。该级别可以按类别设置,也可以常规设置,或两者都设置。如果用户未设置 verbosity,则使用级别 100(?MAX_VERBOSITY = 所有输出可见)作为默认值。Common Test 执行以下测试

Importance >= (100-VerbosityLevel)

常量 ?STD_VERBOSITY 的值为 50(请参阅 ct.hrl)。在此级别,将打印所有标准 I/O。如果设置较低的详细级别,则会忽略标准 I/O 输出。详细级别 0 有效地关闭所有日志记录(来自 Common Test 本身进行的输出除外)。

常规详细级别不与任何特定类别相关联。此级别为标准 I/O 输出、未分类的 ct:log/print/pal 输出以及具有未定义详细级别的类别的输出设置阈值。

示例

测试用例执行期间的一些输出

io:format("1. Standard IO, importance = ~w~n", [?STD_IMPORTANCE]),
ct:log("2. Uncategorized, importance = ~w", [?STD_IMPORTANCE]),
 ct:log(info, "3. Categorized info, importance = ~w", [?STD_IMPORTANCE]),
 ct:log(info, ?LOW_IMPORTANCE, "4. Categorized info, importance = ~w", [?LOW_IMPORTANCE]),
 ct:log(error, ?HI_IMPORTANCE, "5. Categorized error, importance = ~w", [?HI_IMPORTANCE]),
 ct:log(error, ?MAX_IMPORTANCE, "6. Categorized error, importance = ~w", [?MAX_IMPORTANCE]),

如果以 50(?STD_VERBOSITY)的常规详细级别启动测试

$ ct_run -verbosity 50

则会打印以下内容

1. Standard IO, importance = 50
2. Uncategorized, importance = 50
3. Categorized info, importance = 50
5. Categorized error, importance = 75
6. Categorized error, importance = 99

如果以

$ ct_run -verbosity 1 and info 75

则会打印以下内容

3. Categorized info, importance = 50
4. Categorized info, importance = 25
6. Categorized error, importance = 99

请注意,为了仅指定输出的重要性,不需要类别参数。示例

ct:pal(?LOW_IMPORTANCE, "Info report: ~p", [Info])

或者可能与常量结合使用

-define(INFO, ?LOW_IMPORTANCE).
-define(ERROR, ?HI_IMPORTANCE).

ct:log(?INFO, "Info report: ~p", [Info])
ct:pal(?ERROR, "Error report: ~p", [Error])

函数 ct:set_verbosity/2ct:get_verbosity/1 可以用于在测试执行期间修改和读取详细级别。

ct:log/print/pal 中的参数 FormatFormatArgs 始终传递到 STDLIB 函数 io:format/3(有关详细信息,请参阅 io 手册页)。

ct:pal/4ct:log/5 将标头添加到打印到日志文件的字符串。这些字符串还包装在具有 CSS 类属性的 div 标签中,以便可以应用样式表格式。要禁用此功能的输出(即获得类似于使用 io:format/2 的结果),请使用 no_css 选项调用 ct:log/5

如何在“运行测试和分析结果”部分中的 HTML 样式表部分中记录类别如何映射到 CSS 标签。

Common Test 将使用 ct:pal/4io:format/2 打印到日志文件的输出中的特殊 HTML 字符(<、> 和 &)转义。为了将带有 HTML 标签的字符串打印到日志中,请使用 ct:log/3,4,5 函数。默认情况下,字符转义功能对于 ct:log/3,4,5 禁用,但是可以使用 Opts 列表中的 esc_chars 选项启用,请参阅 ct:log/3,4,5

如果需要禁用字符转义功能(通常是出于向后兼容的原因),请使用 ct_run 启动标志 -no_esc_charsct:run_test/1 启动选项 {esc_chars,Bool}(此启动选项在测试规范中也受支持)。

有关日志文件的更多信息,请参阅“运行测试和分析结果”部分中的 日志文件部分。

非法依赖项

即使使用 Common Test 框架编写测试套件非常高效,但也可能会犯错误,主要是因为存在非法依赖项。以下是我们自己运行 Erlang/OTP 测试套件的经验中更常见的错误

  • 依赖于当前目录并在其中写入

    这是测试套件中的常见错误。假设当前目录与作者在开发测试用例时用作当前目录的目录相同。许多测试用例甚至尝试将临时文件写入此目录。相反,应使用 data_dirpriv_dir 来查找数据并写入临时文件。

  • 依赖于执行顺序

    在开发测试套件期间,不要对测试用例或套件的执行顺序进行任何假设。例如,测试用例不得假设它所依赖的服务器已由之前的测试用例启动。原因如下

    • 用户/操作员可以随意指定顺序,并且有时不同的执行顺序可能更相关或更高效。
    • 如果用户为测试指定了整个测试套件目录,则套件的执行顺序取决于操作系统如何列出文件,这在不同的系统之间有所不同。
    • 如果用户只想运行测试套件的子集,则一个测试用例无法成功依赖于另一个测试用例。
  • 依赖于 Unix

    通过 os:cmd 运行 Unix 命令可能无法在非 Unix 平台上工作。

  • 嵌套测试用例

    从另一个测试用例启动测试用例不仅会重复测试同一内容,而且还会使跟踪正在测试的内容更加困难。此外,如果被调用的测试用例因某种原因失败,调用方也会失败。这样,一个错误会导致多个错误报告,这是应该避免的。

    许多测试用例函数通用的功能可以在通用帮助函数中实现。如果这些函数对于跨套件的测试用例有用,请将帮助函数放入通用帮助模块中。

  • 在出现问题时未能崩溃或退出

    如果在测试用例稍后失败的情况下,在不检查返回值是否指示成功的情况下发出请求是可以的,但仅打印错误消息(到日志文件中)并成功返回是绝对不可接受的。这种测试用例会造成损害,因为它们在查看测试结果时会产生一种虚假的安全感。

  • 扰乱后续测试用例

    测试用例应尽可能恢复执行环境,以便后续测试用例不会因其执行顺序而崩溃。函数 end_per_testcase 适用于此目的。