跳到主要内容

xunit

基于GS语言的现代化单元测试框架,参考.NET xunit框架设计理念,专为M95项目开发的通用测试解决方案。

✨ 核心特性

  • 🎯 命名约定驱动: 基于函数名前缀自动识别测试类型,无需装饰器注册
  • 📊 多种测试类型: 支持Fact、Theory(参数化)、Benchmark测试
  • 📈 基准测试: 内置性能测试支持,包含统计分析
  • 📋 多格式报告: 支持控制台、JSON、JUnit XML格式输出
  • 📁 按文件分组: 控制台输出按源文件分组显示,便于查看
  • 🔗 CI/CD集成: 生成JUnit XML格式,支持GitLab CI集成
  • 🛠️ 测试套件管理: 支持通过get_suite_names()函数定义测试套件
  • ⚙️ 灵活配置: 支持并行执行、超时控制等多种配置选项

🚀 快速开始

1. 基本测试示例

import src.xunit;

// 可选:定义测试套件名称列表
public array get_suite_names()
{
return ["用户管理", "产品管理", "系统工具"];
}

// 基本的事实测试
public void test_user_creation()
{
string username = "testuser";
string email = "test@example.com";

xassert.not_null(username, "用户名不能为空");
xassert.contains(email, "@", "邮箱格式不正确");
}

// 另一个事实测试
public void test_user_validation()
{
string email = "test@example.com";

xassert.success(email.contains("@"), "邮箱必须包含@符号");
xassert.success(email.contains("."), "邮箱必须包含域名");
}

// 参数化测试
public void theory_user_age_validation(int age, bool expected)
{
bool is_valid = (age > 0 && age < 120);
xassert.equal(expected, is_valid, sprintf("年龄 %d 的验证结果不匹配", age));
}

// 测试数据提供者
public array theory_user_age_validation_data()
{
return [
[ 25, true ], // 正常年龄
[ -1, false ], // 负数年龄
[ 0, false ], // 零年龄
[ 150, false ], // 超大年龄
];
}

// 基准测试
public void bench_user_search_performance()
{
// 性能测试代码
array users = generate_test_users(1000);
search_users_by_name(users, "张三");
}

// 全局前置函数(如果需要的话)
public void setup()
{
printf("初始化测试环境\n");
// 准备测试数据
}

// 全局后置函数(如果需要的话)
public void teardown()
{
printf("清理测试环境\n");
// 清理测试数据
}

// 主函数:运行测试
void create()
{
// 配置框架
xunit.configure({
"verbose": true,
"output_format": "console"
});

// 自动发现并注册测试
int count = xunit.discover_tests_by_naming_convention(this_object());
printf("发现 %d 个测试用例\n", count);

// 运行所有测试
map report = xunit.run_all_tests({});

// 输出结果
printf("测试完成 - 通过: %d, 失败: %d, 跳过: %d\n",
report["summary"]["passed"],
report["summary"]["failed"],
report["summary"]["skipped"]);
}

2. 运行测试

通过--script-args传递命令行参数:

# 基本运行
gs.exe /r . /eq your_test_file.gs

# 详细输出
gs.exe /r . /eq your_test_file.gs --script-args "--verbose"

# 按套件过滤执行
gs.exe /r . /eq your_test_file.gs --script-args "--suite 用户管理"

# 按多个套件过滤执行
gs.exe /r . /eq your_test_file.gs --script-args "--suite 用户管理,产品管理"

# 按测试用例名称过滤
gs.exe /r . /eq your_test_file.gs --script-args "--filter test_user_creation"

# 按多个测试用例过滤
gs.exe /r . /eq your_test_file.gs --script-args "--filter test_user_creation,test_user_validation"

# 只运行基准测试
gs.exe /r . /eq your_test_file.gs --script-args "--benchmark-only"

# 组合使用:详细输出 + 套件过滤
gs.exe /r . /eq your_test_file.gs --script-args "--verbose --suite 用户管理"

# 并行执行测试
gs.exe /r . /eq your_test_file.gs --script-args "--parallel --verbose"

# 设置超时时间
gs.exe /r . /eq your_test_file.gs --script-args "--timeout 60"

# 生成XML报告(用于CI/CD)
gs.exe /r . /eq your_test_file.gs --script-args "--format xml"

# 组合性能测试:只运行基准测试,详细输出,设置迭代次数
gs.exe /r . /eq your_test_file.gs --script-args "--benchmark-only --verbose --benchmark-iterations 1000"

📖 测试类型详解

Fact测试(事实测试)

用于验证固定逻辑的简单测试,无需参数。

命名约定: test_*fact_*should_*

public void test_user_creation()
{
string username = create_user("testuser");
xassert.equal("testuser", username, "用户名应该正确");
}

public void should_validate_email()
{
bool result = is_valid_email("test@example.com");
xassert.success(result, "有效邮箱应该通过验证");
}

Theory测试(参数化测试)

支持多组数据的参数化测试。

命名约定: theory_*

public void theory_login_validation(string username, string password, bool expected)
{
bool result = validate_login(username, password);
xassert.equal(expected, result, "登录验证结果不匹配");
}

// 数据提供者函数:测试函数名 + "_data"
public array theory_login_validation_data()
{
return [
["admin", "password123", true],
["user", "wrongpass", false],
["", "password", false],
["admin", "", false]
];
}

Benchmark测试(基准测试)

自动测量性能指标的测试类型。

命名约定: bench_*benchmark_*perf_*

public void bench_string_operations()
{
string result = "";
for (int i = 0; i < 1000; i++)
{
result += "test";
}
}

public void benchmark_array_operations()
{
// 测试数组操作性能
array arr = [];
for (int i = 0; i < 1000; i++)
{
arr.push_back(i);
}
}

// 自定义迭代次数
public void perf_iter_5000_map_operations()
{
// 这个基准测试会运行5000次迭代
map data = {};
for (int i = 0; i < 100; i++)
{
data[sprintf("key_%d", i)] = i;
}
}

// 自定义预热次数
public void bench_warmup_200_function_call()
{
// 这个基准测试会有200次预热
expensive_function_call();
}

// 同时设置迭代和预热次数
public void benchmark_iter_2000_warmup_50_sorting()
{
// 2000次迭代,50次预热
array data = generate_random_array(100);
sort_array(data);
}

设置迭代次数 在函数名中包含 _iter_数字_ 标记:

public void perf_iter_5000_map_operations()
{
// 这个基准测试会运行5000次迭代
map data = {};
data["key"] = "value";
}

设置预热次数 在函数名中包含 _warmup_数字_ 标记:

public void bench_warmup_200_function_call()
{
// 这个基准测试会有200次预热
cached_function_call();
}

同时设置迭代和预热次数

public void benchmark_iter_2000_warmup_50_sorting()
{
// 2000次迭代,50次预热
array data = [3, 1, 4, 1, 5, 9, 2, 6];
data.sort();
}

🔧 测试元数据

通过在函数名中添加特殊标记,可以设置测试的元数据属性:

跳过测试

在函数名中包含 _skip_ 标记:

public void test_skip_incomplete_feature()
{
// 这个测试会被自动跳过
}

设置超时时间

在函数名中包含 _timeout_数字_ 标记:

public void test_timeout_30_slow_operation()
{
// 这个测试有30秒的超时时间
slow_database_operation();
}

🔧 测试套件

测试套件通过get_suite_names()函数来定义:

// 定义测试套件名称列表(可选)
public array get_suite_names()
{
return ["用户管理", "产品管理", "系统工具"];
}

框架支持全局的前置后置函数:

// 全局前置函数
public void setup()
{
// 测试前的准备工作
}

// 全局后置函数
public void teardown()
{
// 测试后的清理工作
}

⚙️ 配置选项

xunit.configure({
// 基础设置
"parallel": false, // 是否并行执行
"timeout": 30, // 超时时间(秒)
"verbose": false, // 详细输出
"output_format": "console", // 输出格式:console/json/xml
"base_path": ".", // 基础路径

// 测试发现设置
"auto_discover": true, // 自动发现测试
"test_patterns": [ // 测试文件匹配模式
"**/*Test.gs",
"**/*_test.gs",
"**/test_*.gs"
],
"fact_prefixes": [ // Fact测试前缀
"test_", "fact_", "should_"
],
"theory_prefixes": ["theory_"], // Theory测试前缀
"benchmark_prefixes": [ // Benchmark测试前缀
"bench_", "benchmark_", "perf_"
],

// 基准测试设置
"benchmark_iterations": 100, // 默认迭代次数
"benchmark_warmup": 0, // 默认预热次数

// 报告输出配置
"xml_output_file": "test-results.xml", // JUnit XML输出文件
"json_output_file": "test-results.json", // JSON输出文件
"console_group_by_file": true, // 控制台按文件分组显示
"show_file_paths": true, // 显示文件路径信息
});

📊 断言库 (xassert)

基本断言

xassert.equal(expected, actual, message);
xassert.not_equal(expected, actual, message);
xassert.success(condition, message);
xassert.failure(condition, message);
xassert.null(value, message);
xassert.not_null(value, message);

字符串断言

xassert.contains(text, substring, message);
xassert.not_contains(text, substring, message);
xassert.starts_with(text, prefix, message);
xassert.ends_with(text, suffix, message);

数值断言

xassert.greater_than(actual, expected, message);
xassert.less_than(actual, expected, message);
xassert.in_range(actual, min, max, message);

集合断言

xassert.array_length(array, expected_length, message);
xassert.array_contains(array, item, message);
xassert.map_contains_key(map, key, message);

📋 报告格式

控制台报告

xunit.configure({"output_format": "console"});

输出示例:

=== XUnit 测试报告 ===
执行时间: 2025-01-08T12:30:45Z
总执行时间: 0.350 秒

测试摘要:
总计: 8
通过: 6 (75.0%)
失败: 2 (25.0%)

✅ test/user_test.gs
总计: 3, 通过: 3, 失败: 0, 耗时: 0.120s
✅ test_user_creation (0.030s)
✅ test_user_login (0.050s)
✅ test_user_logout (0.040s)

❌ test/auth_test.gs
总计: 2, 通过: 1, 失败: 1, 耗时: 0.080s
✅ test_authentication (0.030s)
❌ test_authorization (0.050s)
💬 Expected true but got false

JSON报告

xunit.configure({
"output_format": "json",
"json_output_file": "test-results.json"
});

JUnit XML报告(CI/CD集成)

xunit.configure({
"output_format": "xml",
"xml_output_file": "test-results.xml"
});

基准测试报告

基准测试会自动生成详细的性能报告:

=== 基准测试报告 ===
总计基准测试: 5
成功: 5, 失败: 0

测试名称 平均时间(s) 最小时间(s) 最大时间(s) 操作/秒
------------------------------ ------------ ------------ ------------ ------------
bench_string_operations 0.000012 0.000010 0.000015 83333.33
benchmark_array_operations 0.000008 0.000007 0.000010 125000.00
perf_iter_5000_map_operations 0.000006 0.000005 0.000008 166666.67
bench_warmup_200_function_call 0.000003 0.000002 0.000004 333333.33
benchmark_iter_2000_warmup_50_sorting 0.000015 0.000012 0.000018 66666.67

🔧 命令行参数详解

XUnit测试框架支持多种命令行参数,通过--script-args传递:

基本参数

参数简写说明示例
--verbose-v启用详细输出--script-args "--verbose"
--parallel-p并行执行测试--script-args "--parallel"
--timeout-t设置超时时间(秒)--script-args "--timeout 60"

过滤参数

参数简写说明示例
--suite-s按套件名称过滤--script-args "--suite 用户管理,产品管理"
--filter按测试用例名称过滤--script-args "--filter test_user_creation,test_user_login"
--benchmark-only-b只运行基准测试--script-args "--benchmark-only"

输出格式参数

参数简写说明示例
--format-f输出格式(console/json/xml)--script-args "--format xml"

基准测试参数

参数说明示例
--benchmark-iterations设置基准测试迭代次数--script-args "--benchmark-iterations 1000"
--benchmark-warmup设置基准测试预热次数--script-args "--benchmark-warmup 100"

高级参数

参数说明示例
--base-path设置基础路径--script-args "--base-path ./tests"
--pattern设置测试文件匹配模式--script-args "--pattern *_test.gs,test_*.gs"
--no-auto-discover禁用自动发现--script-args "--no-auto-discover"

使用场景示例

1. 按套件过滤执行

# 只运行"用户管理"套件的测试
gs.exe /r . /eq test.gs --script-args "--suite 用户管理"

# 运行多个套件
gs.exe /r . /eq test.gs --script-args "--suite 用户管理,产品管理,系统工具"

# 详细输出套件测试结果
gs.exe /r . /eq test.gs --script-args "--suite 用户管理 --verbose"

2. 按测试用例名称过滤

# 只运行特定测试用例
gs.exe /r . /eq test.gs --script-args "--filter test_user_creation"

# 运行多个测试用例
gs.exe /r . /eq test.gs --script-args "--filter test_user_creation,test_user_validation,theory_user_age_validation"

# 组合过滤:特定套件中的特定测试
gs.exe /r . /eq test.gs --script-args "--suite 用户管理 --filter test_user_creation,test_user_login"

3. 组合性能测试

# 基本基准测试
gs.exe /r . /eq test.gs --script-args "--benchmark-only"

# 高级基准测试:设置迭代次数和预热次数
gs.exe /r . /eq test.gs --script-args "--benchmark-only --benchmark-iterations 5000 --benchmark-warmup 200"

# 只运行特定的基准测试
gs.exe /r . /eq test.gs --script-args "--benchmark-only --filter bench_user_search_performance"

# 详细基准测试报告
gs.exe /r . /eq test.gs --script-args "--benchmark-only --verbose --benchmark-iterations 2000"

# 并行基准测试(小心使用,可能影响性能测试准确性)
gs.exe /r . /eq test.gs --script-args "--benchmark-only --parallel --verbose"

4. CI/CD集成

# 生成JUnit XML报告
gs.exe /r . /eq test.gs --script-args "--format xml"

# 生成JSON报告
gs.exe /r . /eq test.gs --script-args "--format json"

# GitLab CI使用:详细输出 + XML报告
gs.exe /r . /eq test.gs --script-args "--verbose --format xml"

5. 调试和开发

# 调试模式:详细输出 + 特定测试 + 延长超时
gs.exe /r . /eq test.gs --script-args "--verbose --filter test_problematic_function --timeout 120"

# 快速验证:并行执行 + 简化输出
gs.exe /r . /eq test.gs --script-args "--parallel"

# 完整测试:所有测试 + 详细输出 + 性能测试
gs.exe /r . /eq test.gs --script-args "--verbose"
gs.exe /r . /eq test.gs --script-args "--benchmark-only --verbose"

🔗 GitLab CI/CD 集成

.gitlab-ci.yml 示例

test:
stage: test
script:
- gs.exe /r . /eq test/all_tests.gs --script-args "--format xml --verbose"
artifacts:
reports:
junit: test-results.xml
paths:
- test-results.xml
when: always

# 分别运行单元测试和性能测试
test_unit:
stage: test
script:
- gs.exe /r . /eq test/all_tests.gs --script-args "--format xml --verbose"
artifacts:
reports:
junit: test-results.xml
when: always

test_performance:
stage: test
script:
- gs.exe /r . /eq test/all_tests.gs --script-args "--benchmark-only --format xml --verbose"
artifacts:
reports:
junit: benchmark-results.xml
when: always
allow_failure: true # 性能测试失败不阻止流水线

# 按套件分别测试
test_user_module:
stage: test
script:
- gs.exe /r . /eq test/all_tests.gs --script-args "--suite 用户管理 --format xml --verbose"
artifacts:
reports:
junit: user-test-results.xml
when: always

test_product_module:
stage: test
script:
- gs.exe /r . /eq test/all_tests.gs --script-args "--suite 产品管理 --format xml --verbose"
artifacts:
reports:
junit: product-test-results.xml
when: always

测试脚本示例

// test/all_tests.gs
import src.xunit;

// 定义测试套件名称
public array get_suite_names()
{
return ["用户管理", "产品管理", "系统工具"];
}

// 测试用例...
public void test_user_creation() { /* 测试实现 */ }
public void theory_user_validation(int age, bool expected) { /* 测试实现 */ }
public void bench_user_search_performance() { /* 基准测试实现 */ }

void create()
{
// 框架会自动通过get_script_args()获取--script-args传递的参数
// 并通过内置的parse_args()函数解析
xunit.main(); // 使用框架的main()方法,它会自动处理所有参数
}

// 或者如果需要自定义处理:
void create_custom()
{
// 获取脚本参数(由框架的get_script_args()提供)
array script_args = get_script_args();

// 手动解析参数(可选,通常使用xunit.main()即可)
map config = {};
bool verbose = false;
string output_format = "console";
array suite_filter = [];
array test_filter = [];

for (int i = 0; i < lengthof(script_args); i++)
{
string arg = script_args[i];
switch (arg)
{
case "--verbose":
case "-v":
verbose = true;
break;
case "--format":
case "-f":
if (i + 1 < lengthof(script_args))
{
output_format = script_args[++i];
}
break;
case "--suite":
case "-s":
if (i + 1 < lengthof(script_args))
{
suite_filter = script_args[++i].explode(",");
}
break;
case "--filter":
if (i + 1 < lengthof(script_args))
{
test_filter = script_args[++i].explode(",");
}
break;
}
}

// 配置框架
xunit.configure({
"verbose": verbose,
"output_format": output_format
});

// 发现测试
int count = xunit.discover_tests_by_naming_convention(this_object());
printf("发现 %d 个测试用例\n", count);

// 运行测试
map filter = {
"suite_names": suite_filter,
"test_names": test_filter
};

map results = xunit.run_all_tests(filter);

// 输出结果并设置退出码
printf("测试完成 - 通过: %d, 失败: %d\n",
results["summary"]["passed"],
results["summary"]["failed"]);

if (results["summary"]["failed"] > 0)
{
exit(1);
}
}

📚 API 参考

核心函数

// 配置测试框架
void configure(map config);

// 自动发现测试用例
int discover_tests_by_naming_convention(object test_object);

// 运行所有测试
map run_all_tests(map options);

// 获取所有测试套件
array get_all_test_suites();

// 获取测试统计信息
map get_test_statistics();

// 运行基准测试
map run_benchmark_tests(map options);

📁 项目结构

xunit/
├── src/ # 核心源代码
│ ├── xunit.gs # 主框架
│ ├── test_discoverer.gs # 测试发现器
│ ├── test_runner.gs # 测试运行器
│ ├── test_reporter.gs # 报告生成器
│ ├── test_registry.gs # 测试注册表
│ └── xassert.gs # 断言库
├── sample/ # 示例代码
│ ├── naming_convention_test.gs
│ ├── test_suite_example.gs
│ └── test_suite_simplified.gs
├── docs/ # 文档
│ ├── 开发与使用指南.md
│ ├── 命名约定测试指南.md
│ └── 实现摘要.md
├── test/ # 框架测试
│ └── test.gs
├── package.json # 包信息
└── README.md # 项目说明

💡 最佳实践

1. 测试组织

  • 按功能模块划分测试文件
  • 使用清晰的函数命名描述测试意图
  • 每个suite包含5-20个相关测试

2. 断言消息

// ✅ 好的做法
xassert.equal(expected, actual, sprintf("用户 %s 的年龄应该是 %d", user_name, expected));

// ❌ 避免
xassert.equal(expected, actual, "值不匹配");

3. 数据提供者

// ✅ 清晰的测试数据
public array theory_user_validation_data()
{
return [
[ "valid_user", "user@example.com", true ],
[ "invalid_user", "invalid_email", false ],
[ "", "user@example.com", false ]
];
}

4. Setup/Teardown

// ✅ 确保清理
public void user_setup()
{
create_test_database();
initialize_test_users();
}

public void user_teardown()
{
cleanup_test_database();
reset_test_environment();
}

🔍 故障排除

常见问题

  1. 测试未被发现

    • 检查函数命名是否符合约定
    • 确认auto_discover配置已启用
    • 使用verbose模式查看详细信息
  2. Setup/Teardown未执行

    • 检查函数命名是否为setupteardown
    • 确认函数为public
    • 确保函数存在于测试对象中
  3. 基准测试结果不稳定

    • 增加迭代次数和预热次数
    • 确保测试环境稳定
    • 避免测试函数中包含无关操作

开始使用 XUnit 测试框架,让GS语言项目的测试变得简单高效! 🚀