一、为什么选择Common Test框架

在Erlang生态中做测试驱动开发(TDD)时,我们有几个测试框架可以选择。但Common Test绝对是工业级应用的首选,原因很简单:它是Erlang/OTP官方自带的测试框架,天生就能完美融入OTP体系。

想象一下,你正在开发一个分布式电话交换系统。每次代码改动都可能影响上百万用户的通话质量。这时候你需要的是:

  1. 可以模拟真实网络环境的测试
  2. 能够并行执行的测试套件
  3. 详尽的日志报告系统
  4. 与CI/CD管道无缝集成

Common Test恰好满足所有这些需求。它最初就是为电信级应用设计的,所以对并发测试、分布式测试的支持是刻在基因里的。下面这个简单的测试模块展示了它的基础结构:

-module(phonebook_test).
-include_lib("common_test/include/ct.hrl").

%% 每个测试套件都必须导出all/0函数
%% 这里列出所有要执行的测试用例
all() -> [test_add_contact, test_search_contact].

%% 测试初始化函数
init_per_testcase(_TestCase, Config) ->
    phonebook:start(),  % 启动被测系统
    Config.

%% 测试清理函数
end_per_testcase(_TestCase, _Config) ->
    phonebook:stop(),   % 停止被测系统
    ok.

%% 测试添加联系人功能
test_add_contact(_Config) ->
    {ok, Id} = phonebook:add_contact("张三", "13800138000"),
    {ok, "13800138000"} = phonebook:search(Id).

%% 测试搜索联系人功能
test_search_contact(_Config) ->
    {ok, Id} = phonebook:add_contact("李四", "13900139000"),
    {error, not_found} = phonebook:search("王五").

这个示例展示了Common Test的几个关键特性:

  1. 明确的测试生命周期管理(init/end函数)
  2. 自描述的测试用例命名
  3. 模式匹配断言(=操作符)
  4. 配置参数传递机制

二、测试套件的组织结构

大型Erlang项目通常有上百个测试用例,良好的组织结构至关重要。Common Test推荐的三层结构在实践中非常有效:

  1. 测试套件(Test Suite):对应被测模块的测试集合
  2. 测试用例组(Test Case Group):相关测试用例的逻辑分组
  3. 单个测试用例(Test Case):最小的测试单元

这里有一个更复杂的示例,展示如何使用测试组和套件级配置:

-module(bank_account_test).
-include_lib("common_test/include/ct.hrl").

all() -> 
    [{group, transaction_tests}, 
     {group, security_tests}].

groups() ->
    [{transaction_tests, [parallel], 
      [test_deposit, test_withdraw, test_transfer]},
     {security_tests, [sequence], 
      [test_auth, test_brute_force_protection]}].

init_per_suite(Config) ->
    bank_db:create_tables(),  % 套件级别初始化
    Config.

end_per_suite(_Config) ->
    bank_db:delete_tables().  % 套件级别清理

init_per_group(transaction_tests, Config) ->
    {ok, Acc} = bank_account:open("测试用户", 1000),
    [{account, Acc} | Config];
init_per_group(security_tests, Config) ->
    Config.

test_deposit(Config) ->
    Acc = ?config(account, Config),
    {ok, 1500} = bank_account:deposit(Acc, 500).

test_withdraw(Config) ->
    Acc = ?config(account, Config),
    {error, insufficient_balance} = bank_account:withdraw(Acc, 2000).

关键点说明:

  1. groups()函数定义了测试组及其属性
  2. parallel表示组内测试可以并行执行
  3. sequence表示组内测试必须按顺序执行
  4. ?config宏用于访问配置数据
  5. 初始化可以在套件、组、用例三个级别进行

三、高级测试技巧

当测试复杂系统时,我们需要更强大的工具。Common Test提供了这些高级特性:

3.1 分布式测试

测试分布式Erlang应用时,可以轻松启动多个节点:

distributed_test(_Config) ->
    NodeNames = [node1@host1, node2@host2],
    [ct_slave:start(Node) || Node <- NodeNames],
    % 在节点间执行测试逻辑
    ok = rpc:call(node1@host1, my_app, start, []),
    ok = rpc:call(node2@host2, my_app, join, [node1@host1]).

3.2 数据驱动测试

使用CSV或Excel文件作为测试数据源:

data_driven_test(Config) ->
    DataFile = ?config(data_dir, Config) ++ "test_cases.csv",
    {ok, Fd} = file:open(DataFile, [read]),
    TestCases = read_test_cases(Fd),
    [run_single_case(Case) || Case <- TestCases].

read_test_cases(Fd) ->
    case file:read_line(Fd) of
        {ok, Line} -> [parse_line(Line) | read_test_cases(Fd)];
        eof -> []
    end.

3.3 性能测试集成

结合tsung进行压力测试:

performance_test(_Config) ->
    ConfigFile = "benchmark.xml",
    os:cmd("tsung -f " ++ ConfigFile ++ " start"),
    % 解析测试结果
    {ok, Summary} = file:read_file("tsung.log"),
    ct:log("Performance result: ~p", [Summary]).

四、最佳实践总结

经过多个大型项目实践,我们总结了这些经验:

  1. 测试金字塔:保持70%的单元测试,20%的集成测试,10%的端到端测试
  2. 命名规范:测试模块加_test后缀,用例用test_前缀
  3. 测试隔离:每个测试用例应该是独立的,不依赖执行顺序
  4. 日志策略:合理使用ct:log/2ct:pal/2输出调试信息
  5. CI集成:在rebar3或Makefile中添加ct_run命令

一个典型的Makefile集成示例:

test:
    ct_run -dir test -logdir logs -cover bank_account.coverspec

最后要强调的是,Common Test虽然强大,但也有其局限性。它不适合做:

  1. 纯前端测试
  2. 细粒度的单元测试(更适合eunit)
  3. 需要复杂mock的场景

但在测试OTP应用、分布式系统、协议实现等方面,它仍然是Erlang生态系统中最可靠的选择。