一、为什么选择Common Test框架
在Erlang生态中做测试驱动开发(TDD)时,我们有几个测试框架可以选择。但Common Test绝对是工业级应用的首选,原因很简单:它是Erlang/OTP官方自带的测试框架,天生就能完美融入OTP体系。
想象一下,你正在开发一个分布式电话交换系统。每次代码改动都可能影响上百万用户的通话质量。这时候你需要的是:
- 可以模拟真实网络环境的测试
- 能够并行执行的测试套件
- 详尽的日志报告系统
- 与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的几个关键特性:
- 明确的测试生命周期管理(init/end函数)
- 自描述的测试用例命名
- 模式匹配断言(=操作符)
- 配置参数传递机制
二、测试套件的组织结构
大型Erlang项目通常有上百个测试用例,良好的组织结构至关重要。Common Test推荐的三层结构在实践中非常有效:
- 测试套件(Test Suite):对应被测模块的测试集合
- 测试用例组(Test Case Group):相关测试用例的逻辑分组
- 单个测试用例(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).
关键点说明:
groups()函数定义了测试组及其属性parallel表示组内测试可以并行执行sequence表示组内测试必须按顺序执行?config宏用于访问配置数据- 初始化可以在套件、组、用例三个级别进行
三、高级测试技巧
当测试复杂系统时,我们需要更强大的工具。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]).
四、最佳实践总结
经过多个大型项目实践,我们总结了这些经验:
- 测试金字塔:保持70%的单元测试,20%的集成测试,10%的端到端测试
- 命名规范:测试模块加
_test后缀,用例用test_前缀 - 测试隔离:每个测试用例应该是独立的,不依赖执行顺序
- 日志策略:合理使用
ct:log/2和ct:pal/2输出调试信息 - CI集成:在rebar3或Makefile中添加
ct_run命令
一个典型的Makefile集成示例:
test:
ct_run -dir test -logdir logs -cover bank_account.coverspec
最后要强调的是,Common Test虽然强大,但也有其局限性。它不适合做:
- 纯前端测试
- 细粒度的单元测试(更适合eunit)
- 需要复杂mock的场景
但在测试OTP应用、分布式系统、协议实现等方面,它仍然是Erlang生态系统中最可靠的选择。
评论