对于测试同学来说,拿AI做测试小工具确实不要太爽

前因后果

最近有一些做测试使用的小工具的需求,拿 AI 生成了一下,发现确实对测试效率有一定的提升。

前几天需要写一个简单的性能压测工具,自己写的话可能要 1-2 天,估计效果还不太好,于是试着拿 AI 去生成了一下。

为了让工具有足够的并发性能,我选择用 rust 语言来实现。

简单描述了一下需求,claude 就在几分钟之内给出了结果。

具体过程

我的提示词非常简单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
How to use rust to implement this cli feature .

it is a stress test cli

apistress rest --url https://api.example.com/users \
               --method POST \
               --body '{"name":"test"}' \
               --header "Authorization: Bearer TOKEN" \
               --concurrency 50 \
               --duration 30s \
               --rate 1000 \
               --output json

不久 claude 给出了第一版的代码。

初版的代码里所有的内容都写在main.rs里面,尽管简单,但可维护性还是不强。

于是我再加强了一下。

这是我的提示词。

1
seperate to more files

告诉 ai 将代码分成多个文件。

这是后来的文件目录。

1
2
3
4
5
6
7
src/main.rs - Entry point that sets up modules and runs the CLI
src/cli.rs - Command line interface definition using clap
src/config.rs - Request configuration and parsing
src/client.rs - HTTP client functionality
src/results.rs - Test results and reporting
src/rate_limiter.rs - Rate limiting implementation
src/stress_test.rs - Main stress testing orchestration

逻辑上清楚多了。

根据代码编译了一下,然后随手写了个 api 试试效果。

api 应用是用 golang+gin 去写的,代码非常简单。

1
2
3
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "pong"})
	})

拿 ai 生成的工具去试试效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
stress_test rest --url http://localhost:8080/ping --duration 10s

[00:00:10] Test completed
=== Stress Test Results ===
Total Requests: 273398
Successful: 273398
Failed: 0
Test Duration: 10.00s
Requests/sec: 27339.80

Latency:
  Min: 0.06ms
  Max: 33.02ms
  Avg: 0.31ms

Status Codes:
  200: 273398

结果是挺惊艳的,跑出了 27k 的 QPS,性能方面应该是满足我的原始需求的。

横向比较

为了横向比较这个工具的性能,我又用 golang 写了个类似功能的 cli 压力测试工具。

用同样的被测应用跑了一下效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
⠏ Running stress test...  [9s]
=== Stress Test Results ===
Total Requests: 29244
Successful: 29244
Failed: 0
Test Duration: 10.00s
Requests/sec: 2924.40

Latency:
  Min: 0.18ms
  Max: 614.15ms
  Avg: 3.41ms

Status Codes:
  200: 29244

只跑出了 2.9k 的 QPS,基本上性能差了 10 倍。

思考

  • AI 的编程能力已经超过了我能力,我绞尽脑汁写的工具在性能上是完全比不上 ai 几分钟生成的结果的
  • Golang 的性能比 Rust 要差,但在并发上效果差 10 倍,这是不正常的,所以还是印证了上面的观点,ai 的代码写的更合理,比我要强

对于测试同学来说,我们经常会写一些次抛的工具,这些工具可能在某些测试任务上会给我们带来极大的效率提升,比如造数据工具。

但是测试同学的代码能力有限,时间有限,所以一般情况下,我们是不会去写这些次抛工具的。

这就造成了大部分情况下,我们选择用人工的方式去完成这些重复性的工具。

这就是我们测试经常被诟病测试效率低下的一部分原因了。

现在有了 ai,测试同学哪怕不会写代码,也完全有能力把自己的次抛工具需求给描述清楚,毕竟是一次性或者几次性的工具,功能大都不复杂,对 ai 非常友好,所以几个来回就可以把工具给生成出来。

可以预想到,在不远的将来,测试岗位可能会有减少,但熟练使用 ai 并跟测试结合的新型测试人才,应该是会有一定的市场的。

所以从本质上讲,今后的一段时间,发现问题还是靠人,但是 ai 会加速这个过程。

学习时间

是时候看一下这个测试工具中的核心代码了,还是非常有学习的意义的。

Rust 版本的压力测试核心函数:

函数签名解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub async fn run_stress_test(
    url: String,                    // 目标URL
    method: String,                 // HTTP方法 (GET, POST, etc.)
    body: Option<String>,           // 可选的请求体
    headers: Vec<String>,           // 请求头列表
    concurrency: usize,             // 并发数
    duration: String,               // 测试持续时间字符串
    rate: Option<u64>,              // 可选的速率限制
    output: OutputFormat,           // 输出格式
) -> Result<()>                     // 返回Result类型

逐步详细解析

1. 解析和配置阶段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 解析持续时间字符串 "30s" -> Duration对象
let test_duration = humantime::parse_duration(&duration)?;

// 创建请求配置对象,包含所有HTTP请求参数
let config = RequestConfig::new(url, method, body, headers)?;

// 创建HTTP客户端(复用连接池)
let client = Client::new();

// 创建共享的测试结果对象
let results = Arc::new(Mutex::new(TestResult::new()));

为什么用 Arc<Mutex<T>>

  • Arc:原子引用计数,多个线程可以安全地共享所有权
  • Mutex:互斥锁,确保同一时间只有一个线程能修改结果

2. 进度条设置

1
2
let pb = create_progress_bar();
pb.set_message("Running stress test...");

用户可以看到测试正在进行中的实时反馈。

3. 工作线程创建(关键部分)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let mut handles = Vec::new();

for _ in 0..concurrency {  // 创建指定数量的并发工作者
    // 为每个工作者克隆必要的资源
    let client = client.clone();        // 克隆HTTP客户端
    let config = config.clone();        // 克隆请求配置
    let results = Arc::clone(&results); // 克隆共享结果的引用

    // 为每个工作者创建独立的速率限制器
    let rate_limiter = rate.map(|rps| RateLimiter::new(rps / concurrency as u64));

    // 生成异步任务
    let handle = tokio::spawn(async move {
        run_worker(client, config, results, rate_limiter).await;
    });

    handles.push(handle);  // 保存任务句柄以便后续控制
}

速率限制分配的重要细节

1
2
3
// 如果总速率是1000 RPS,10个并发工作者
rate.map(|rps| RateLimiter::new(rps / concurrency as u64));
// 每个工作者得到:1000 / 10 = 100 RPS

4. 时间控制和清理

1
2
3
4
5
6
7
// 让测试运行指定的时间
sleep(test_duration).await;

// 时间到了,强制停止所有工作者
for handle in handles {
    handle.abort();  // 中止异步任务
}

为什么用 abort() 而不是优雅关闭?

  • 压力测试需要精确的时间控制
  • 优雅关闭可能会延长测试时间
  • abort() 确保立即停止

5. 结果收集和输出

1
2
3
4
5
pb.finish_with_message("Test completed");

// 获取最终结果并打印
let final_results = results.lock().await;  // 获取互斥锁
final_results.print_results(test_duration, &output)?;

并发执行流程图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
主线程                     工作线程1            工作线程2            工作线程N
  |                          |                    |                    |
  ├─ 创建配置和客户端         |                    |                    |
  ├─ 创建共享结果对象         |                    |                    |
  ├─ spawn工作线程1 ────────→ 开始发送请求          |                    |
  ├─ spawn工作线程2 ────────────────────────────→ 开始发送请求          |
  ├─ spawn工作线程N ──────────────────────────────────────────────→ 开始发送请求
  ├─ sleep(duration)         │                    │                    │
  │                         │ 持续发送HTTP请求    │ 持续发送HTTP请求    │ 持续发送HTTP请求
  │                         │ 更新共享结果        │ 更新共享结果        │ 更新共享结果
  ├─ 时间到,abort所有任务 ───→ 被强制停止          │                    │
  ├─ ──────────────────────────────────────────→ 被强制停止          │
  ├─ ────────────────────────────────────────────────────────────→ 被强制停止
  ├─ 收集结果
  └─ 打印统计信息

内存安全和并发安全

Rust 的优势:

1
2
3
4
5
6
7
8
9
// 编译时保证线程安全
let results = Arc::new(Mutex::new(TestResult::new()));
let results_clone = Arc::clone(&results);  // 安全的共享

// 在工作线程中安全地更新结果
{
    let mut results = results.lock().await;  // 获取锁
    results.add_request(latency, status, error);
}  // 锁自动释放

对比其他语言可能的问题:

  • C/C++:可能的内存泄漏和数据竞争
  • Java:需要手动管理线程池和同步
  • Go:需要显式的 channel 通信或锁管理

错误处理

1
2
3
4
5
6
7
8
// ? 操作符实现链式错误传播
let config = RequestConfig::new(url, method, body, headers)?;
//                                                        ↑
//                                         如果出错,立即返回错误

final_results.print_results(test_duration, &output)?;
//                                                 ↑
//                                    打印出错也会传播错误

这个函数展现了 Rust 异步编程的强大:零成本抽象内存安全并发安全,同时保持高性能!

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计