目录
[1. 总结接口设计文档](#1. 总结接口设计文档)
[2. 添加管理员账户](#2. 添加管理员账户)
[3. 通过 curl 进行接口自动化测试](#3. 通过 curl 进行接口自动化测试)
[4. 总结接口测试文档](#4. 总结接口测试文档)
[5. 把接口自动化用例整合成 python 程序](#5. 把接口自动化用例整合成 python 程序)
1. 总结接口设计文档
接口测试之前,首先需要明确接口的定义规范。
- 有哪些接口
- 每个接口的作用
- 每个接口的参数
- 每个接口的返回值
@SPEC.md 参考文档,并且结合代码中已经实现的接口,总结一份完整的接口交互文档。包含每个接口的路径、参数、返回的响应结果、作用。总结出的文档写到 API.md 中。
总结出的文档形如:
html
# API 接口文档
## 基础信息
- **Base URL**: `/api`
- **Content-Type**: `application/json`
- **认证方式**: Session/Cookie(通过 `session_id` Cookie)
---
## 公开接口(普通用户)
### 1. 题目列表
**GET** `/api/problems`
获取所有题目列表。
**响应示例** (200):
```json
{
"problems": [
{
"id": 1,
"title": "两数之和",
"difficulty": "Easy",
"created_at": "2026-04-24 10:00:00"
}
]
}
```
---
### 2. 题目详情
**GET** `/api/problems/:id`
获取指定题目的详细信息及测试用例。
**路径参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| id | int | 题目 ID |
**响应示例** (200):
```json
{
"id": 1,
"title": "两数之和",
"difficulty": "Easy",
"content": "给定一个整数数组 nums...",
"template": "#include <iostream>\nusing namespace std;",
"created_at": "2026-04-24 10:00:00",
"test_cases": [
{
"id": 1,
"input": "2 7 11 15",
"expected": "9 15",
"position": 0
}
]
}
```
**错误响应**:
- `400`: `{"error": "invalid problem id"}`
- `404`: `{"error": "problem not found"}`
---
### 3. 提交代码
**POST** `/api/submit`
提交代码并执行。
**请求体**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| code | string | 是 | C++ 源代码 |
| problem_id | int | 是 | 题目 ID |
**响应示例** (200):
```json
{
"status": "AC",
"message": "All test cases passed",
"passed": 3,
"total": 3,
"details": [
{
"case": 1,
"exit_code": 0,
"elapsed_ms": 15,
"stdout": "9 15",
"status": "AC"
},
{
"case": 2,
"exit_code": 0,
"elapsed_ms": 12,
"stdout": "9 15",
"status": "AC",
"expected": "9 15",
"actual": "9 15"
}
]
}
```
**status 枚举值**:
- `AC`: All Correct,所有测试用例通过
- `WA`: Wrong Answer,输出结果错误
- `TLE`: Time Limit Exceeded,超时
- `RE`: Runtime Error,运行时错误
- `CE`: Compilation Error,编译错误
**错误响应**:
- `400`: `{"error": "code is required"}` / `{"error": "problem_id is required"}`
- `404`: `{"error": "problem not found"}`
---
### 4. 用户注册
**POST** `/api/register`
注册新用户账号。
**请求体**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | string | 是 | 用户名,3-64 字符 |
| password | string | 是 | 密码,至少 6 字符 |
**响应示例** (201):
```json
{
"message": "user registered successfully",
"id": 1
}
```
**错误响应**:
- `400`: `{"error": "username is required"}` / `{"error": "username must be between 3 and 64 characters"}` / `{"error": "password must be at least 6 characters"}`
- `409`: `{"error": "username already exists"}`
- `500`: `{"error": "failed to create user"}`
---
### 5. 用户登录
**POST** `/api/login`
用户登录,登录成功后在 Cookie 中设置 `session_id`。
**请求体**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | string | 是 | 用户名 |
| password | string | 是 | 密码 |
**响应示例** (200):
```json
{
"message": "login successful",
"username": "testuser",
"role": "user"
}
```
**错误响应**:
- `400`: `{"error": "username is required"}` / `{"error": "password is required"}`
- `401`: `{"error": "invalid username or password"}`
---
### 6. 用户登出
**POST** `/api/logout`
用户登出,销毁当前会话。
**请求头**:
- `Cookie`: `session_id=xxx`
**响应示例** (200):
```json
{
"message": "logout successful"
}
```
---
## 管理接口(管理员)
> **注意**: 管理接口需要管理员权限(role 为 `admin`)
### 7. 新增题目
**POST** `/api/admin/problems`
创建新题目。
**请求头**:
- `Cookie`: `session_id=xxx`
**请求体**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| title | string | 是 | 题目标题 |
| difficulty | string | 是 | 难度等级:`Easy`、`Medium`、`Hard` |
| content | string | 是 | 题目描述(Markdown 格式) |
| template | string | 否 | 代码模板 |
| test_cases | array | 否 | 测试用例数组 |
**test_cases 数组元素**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| input | string | 是 | 输入数据 |
| expected | string | 是 | 期望输出 |
| position | int | 否 | 排序序号 |
**响应示例** (201):
```json
{
"id": 1,
"message": "problem created successfully"
}
```
**错误响应**:
- `400`: `{"error": "title is required"}` / `{"error": "difficulty is required"}` / `{"error": "content is required"}` / `{"error": "invalid difficulty, must be Easy, Medium, or Hard"}`
- `500`: `{"error": "failed to save problem"}`
---
### 8. 删除题目
**DELETE** `/api/admin/problems/:id`
删除指定题目。
**路径参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| id | int | 题目 ID |
**请求头**:
- `Cookie`: `session_id=xxx`
**响应示例** (200):
```json
{
"message": "problem deleted successfully"
}
```
**错误响应**:
- `400`: `{"error": "invalid problem id"}`
- `500`: `{"error": "failed to delete problem"}`
---
## 数据模型
### 题目 (Problem)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 题目 ID |
| title | string | 题目标题 |
| difficulty | string | 难度:`Easy`、`Medium`、`Hard` |
| content | string | 题目描述 |
| template | string | 代码模板 |
| created_at | string | 创建时间 |
### 测试用例 (TestCase)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 用例 ID |
| problem_id | int | 所属题目 ID |
| input | string | 输入数据 |
| expected | string | 期望输出 |
| position | int | 排序序号 |
### 用户 (User)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 用户 ID |
| username | string | 用户名 |
| role | string | 角色:`user`、`admin` |
| created_at | string | 创建时间 |
---
## 错误码汇总
| HTTP 状态码 | 说明 |
|-------------|------|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 认证失败 |
| 404 | 资源不存在 |
| 409 | 资源冲突(如用户名已存在) |
| 500 | 服务器内部错误 |
根据这份接口文档,就可以进行接口自动化测试了。
正常来说,API接口文档应该是在正式开发之前,就确定好的。
咱们本次开发中,在创建SPEC.md阶段,当时是有看到关于接口的定义,但是没发现此处定义的接口并不完整详细。直到开发了一些功能之后才意识到这个问题。因此只能先完成开发,再补充接口文档了。
各位在开发时,要记得把接口文档提前定义好。
2. 添加管理员账户
此处我们约定,管理员用户名为: admin,密码为 :
由于我们实现了密码加密功能,因此直接在数据库中插入明文密码,是无法正确进行登录的。
而密码加密后的内容,我们又无法手动计算得出。
因此我们需要写一个辅助工具,来构造管理员账户。
实现一个小工具,往数据库中插入管理员账户(用户名是 admin,密码是:),注意密码需要调用加密的逻辑,向数据库中存储密文密码,如果 admin 用户已经存在,删除之前的用户,重新插入这个 admin 用户.
这样的小工具AI开发很快。我们运行这个程序,即可在数据库中插入好管理员信息。
3. 通过 curl 进行接口自动化测试
@API.md 根据当前文档中对于接口的描述,通过 curl 命令的方式来构造 http 请求,验证每个接口是能够正常工作的,服务端已经启动,直接进行测试.
此时,opencode 就会按照文档中的描述,通过curl命令进行接口测试。
注意:由于opencode 在创建后台进程时疑似存在bug,因此需要我们提前在另一个终端中,把oj_server 启动好,并告诉AI直接进行测试。
4. 总结接口测试文档
为了让接口自动化测试的步骤能 "固化" 下来,避免下次测试时由于 AI 的随机性导致测试不通过,我们可以让 AI 总结接口测试的过程。
将上面的步骤,总结成一个 "基于 curl 的接口自动化测试文档.md",便于未来进行重复的测试.
随着程序开发的进展,可能早期的单元测试会收到影响。因此随时多运行单元测试,及时修复,是开发时的一个重要习惯。
总结的文档形如:
html
# 基于 curl 的接口自动化测试文档
## 基础信息
- **Base URL**: `http://localhost:8080/api`
- **Content-Type**: `application/json`
- **认证方式**: Session/Cookie(通过 `session_id` Cookie)
---
## 准备工作
### 启动服务器
```bash
./build/oj_server
```
### 创建 Cookie 文件存放目录
```bash
mkdir -p /tmp
```
---
## 公开接口测试
### 1. 获取题目列表
```bash
curl -s http://localhost:8080/api/problems
```
**预期响应**: 200 OK,返回题目列表
---
### 2. 获取题目详情
```bash
# 正常情况
curl -s http://localhost:8080/api/problems/425
# 题目不存在 (404)
curl -s http://localhost:8080/api/problems/999999
```
**预期响应**:
- 200 OK: 返回题目详情及测试用例
- 404 Not Found: `{"error": "problem not found"}`
---
### 3. 用户注册
```bash
# 正常注册
curl -s -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"123456"}'
# 用户名过短 (400)
curl -s -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"ab","password":"123456"}'
# 用户名已存在 (409)
curl -s -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"123456"}'
```
**预期响应**:
- 201 Created: `{"message": "user registered successfully", "id": 1}`
- 400 Bad Request: `{"error": "username must be between 3 and 64 characters"}`
- 409 Conflict: `{"error": "username already exists"}`
---
### 4. 用户登录
```bash
# 普通用户登录
curl -s -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"123456"}' \
-c /tmp/user_cookies.txt
# 管理员登录
curl -s -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-c /tmp/admin_cookies.txt
```
**预期响应**: 200 OK,返回用户信息及 role
- 普通用户: `{"message": "login successful", "username": "testuser", "role": "user"}`
- 管理员: `{"message": "login successful", "username": "admin", "role": "admin"}`
---
### 5. 用户登出
```bash
curl -s -X POST http://localhost:8080/api/logout -b /tmp/user_cookies.txt
```
**预期响应**: 200 OK: `{"message": "logout successful"}`
---
### 6. 提交代码
```bash
# 正常提交
curl -s -X POST http://localhost:8080/api/submit \
-H "Content-Type: application/json" \
-d '{
"code": "#include <iostream>\nusing namespace std;\nint main() { int a,b; cin>>a>>b; cout<<a+b<<endl; return 0; }",
"problem_id": 425
}'
# 缺少 code 字段 (400)
curl -s -X POST http://localhost:8080/api/submit \
-H "Content-Type: application/json" \
-d '{"problem_id": 425}'
# 题目不存在 (404)
curl -s -X POST http://localhost:8080/api/submit \
-H "Content-Type: application/json" \
-d '{"code": "#include <iostream>", "problem_id": 999999}'
```
**预期响应**:
- 200 OK: 返回执行结果,包含 status (AC/WA/TLE/RE/CE)、passed/total 数量及详细结果
- 400 Bad Request: `{"error": "code is required"}`
- 404 Not Found: `{"error": "problem not found"}`
---
## 管理接口测试(需要管理员权限)
### 7. 创建题目
```bash
# 完整参数
curl -s -X POST http://localhost:8080/api/admin/problems \
-H "Content-Type: application/json" \
-b /tmp/admin_cookies.txt \
-d '{
"title": "两数之和",
"difficulty": "Easy",
"content": "给定一个整数数组...",
"template": "#include <iostream>",
"test_cases": [
{"input": "2 7 11 15", "expected": "9", "position": 0},
{"input": "1 2 3", "expected": "3", "position": 1}
]
}'
# 最小参数
curl -s -X POST http://localhost:8080/api/admin/problems \
-H "Content-Type: application/json" \
-b /tmp/admin_cookies.txt \
-d '{"title": "Test", "difficulty": "Easy", "content": "Test content"}'
# 缺少必填字段 (400)
curl -s -X POST http://localhost:8080/api/admin/problems \
-H "Content-Type: application/json" \
-b /tmp/admin_cookies.txt \
-d '{"title": "Test"}'
# 难度值无效 (400)
curl -s -X POST http://localhost:8080/api/admin/problems \
-H "Content-Type: application/json" \
-b /tmp/admin_cookies.txt \
-d '{"title": "Test", "difficulty": "Invalid", "content": "Test"}'
```
**预期响应**:
- 201 Created: `{"id": 1, "message": "problem created successfully"}`
- 400 Bad Request: `{"error": "title is required"}` / `{"error": "invalid difficulty, must be Easy, Medium, or Hard"}`
---
### 8. 删除题目
```bash
# 正常删除
curl -s -X DELETE http://localhost:8080/api/admin/problems/431 \
-b /tmp/admin_cookies.txt
# 无效 ID (400)
curl -s -X DELETE http://localhost:8080/api/admin/problems/invalid \
-b /tmp/admin_cookies.txt
# 题目不存在 (500)
curl -s -X DELETE http://localhost:8080/api/admin/problems/999999 \
-b /tmp/admin_cookies.txt
```
**预期响应**:
- 200 OK: `{"message": "problem deleted successfully"}`
- 400 Bad Request: `{"error": "invalid problem id"}`
- 500 Internal Server Error: `{"error": "failed to delete problem"}`
---
## 权限测试
### 测试非管理员用户访问管理接口
```bash
# 先用普通用户登录
curl -s -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"123456"}' \
-c /tmp/user_cookies.txt
# 尝试创建题目(应失败)
curl -s -X POST http://localhost:8080/api/admin/problems \
-H "Content-Type: application/json" \
-b /tmp/user_cookies.txt \
-d '{"title": "Hack", "difficulty": "Easy", "content": "Test"}'
```
---
## 一键测试脚本
将以下内容保存为 `test_api.sh`,可直接运行:
```bash
#!/bin/bash
BASE_URL="http://localhost:8080/api"
echo "=== 1. 测试获取题目列表 ==="
curl -s "$BASE_URL/problems" | jq .
echo -e "\n=== 2. 测试获取题目详情 ==="
curl -s "$BASE_URL/problems/425" | jq .
echo -e "\n=== 3. 测试用户注册 ==="
curl -s -X POST "$BASE_URL/register" \
-H "Content-Type: application/json" \
-d '{"username":"curluser","password":"123456"}' | jq .
echo -e "\n=== 4. 测试用户登录 ==="
curl -s -X POST "$BASE_URL/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-c /tmp/admin_cookies.txt | jq .
echo -e "\n=== 5. 测试提交代码 ==="
curl -s -X POST "$BASE_URL/submit" \
-H "Content-Type: application/json" \
-d '{"code":"#include <iostream>\nusing namespace std;\nint main(){int a,b;cin>>a>>b;cout<<a+b<<endl;return 0;}","problem_id":425}' | jq .
echo -e "\n=== 6. 测试创建题目 ==="
curl -s -X POST "$BASE_URL/admin/problems" \
-H "Content-Type: application/json" \
-b /tmp/admin_cookies.txt \
-d '{"title":"Curl Test","difficulty":"Easy","content":"Test"}' | jq .
echo -e "\n=== 7. 测试删除题目 ==="
curl -s -X DELETE "$BASE_URL/admin/problems/432" \
-b /tmp/admin_cookies.txt | jq .
echo -e "\n=== 8. 测试登出 ==="
curl -s -X POST "$BASE_URL/logout" -b /tmp/admin_cookies.txt | jq .
```
---
## 错误码汇总
| HTTP 状态码 | 说明 |
|-------------|------|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 认证失败 |
| 404 | 资源不存在 |
| 409 | 资源冲突(如用户名已存在) |
| 500 | 服务器内部错误 |
---
## status 枚举值
| 值 | 说明 |
|----|------|
| AC | All Correct,所有测试用例通过 |
| WA | Wrong Answer,输出结果错误 |
| TLE | Time Limit Exceeded,超时 |
| RE | Runtime Error,运行时错误 |
| CE | Compilation Error,编译错误 |
5. 把接口自动化用例整合成 python 程序
通过LLM基于文档的方式进行接口测试,虽然比较灵活,但是也存在两个问题:
- 每次运行都需要消耗一定的token
- 由于LLM的执行存在一定的"随机性",每次自行测试的结果不一定稳定。
因此,我们也可以把上述的测试过程,转换成传统的,基于 python 方式的接口自动化测试。
根据 @基于curl的接口自动化测试文档.md 中的内容,把接口自动化测试,通过 python 程序来进行实现,测试代码放到 @tests/python/ 中.
此时,opencode 会基于 requests 和 pytest 构建接口自动化测试,这个过程需要安装一些依赖:
bash
pip install -r requirements.txt --break-system-packages
注意:这里使用**--break-system-packages** 选项,是因为 python 默认不建议把第三方库进行全局安装(为了防止出现版本冲突)。但是由于我们此处并不涉及多个版本库共存的场景,因此直接加上上述选项,强制进行全局安装即可。
接下来 opencode 就会根据前面总结的接口测试的步骤文档,进行测试代码的开发。
开发之后,第一版程序大概率会跑不过(很多响应的细节,python 代码中不一定一步到位),因此直接在 opencode 中运行测试
我已经安装改好了 python 的依赖,直接运行 pytest