测试用例设计与编写完整指南

前言

测试用例是软件测试的核心组成部分,是验证软件功能、性能和质量的重要手段。一个好的测试用例不仅能够发现软件缺陷,还能确保软件满足用户需求和业务要求。本文将全面介绍测试用例的设计原则、编写方法、管理策略以及最佳实践,帮助测试工程师和开发人员掌握测试用例的核心技能。

一、测试用例基础概念

1.1 什么是测试用例

测试用例(Test Case)是为了验证软件系统的特定功能或特性而设计的一组测试步骤、测试数据和预期结果的集合。

测试用例的组成要素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
测试用例 = {
用例编号: "TC_001",
用例标题: "用户登录功能验证",
测试目标: "验证用户能够正常登录系统",
前置条件: "用户已注册且账号状态正常",
测试步骤: [
"1. 打开登录页面",
"2. 输入正确的用户名和密码",
"3. 点击登录按钮"
],
测试数据: {
用户名: "testuser@example.com",
密码: "Password123"
},
预期结果: "用户成功登录,跳转到主页面",
实际结果: "",
测试状态: "待执行",
优先级: "高",
执行人: "张三",
执行时间: "2025-07-18"
}

1.2 测试用例的价值

质量保证价值

  • 缺陷发现:系统性地发现软件缺陷
  • 需求验证:确保软件满足业务需求
  • 回归测试:确保新功能不影响现有功能
  • 风险控制:降低软件发布风险

项目管理价值

  • 进度跟踪:通过用例执行率监控测试进度
  • 质量度量:通过缺陷密度评估软件质量
  • 资源规划:估算测试工作量和时间
  • 沟通工具:团队间的沟通和协作基础

1.3 测试用例分类

按测试层级分类

1
2
3
4
5
6
7
8
9
10
11
测试金字塔:
┌─────────────────┐
│ E2E测试 │ ← 少量,高价值
│ (端到端测试) │
├─────────────────┤
│ 集成测试 │ ← 适量,关键路径
│ (API测试) │
├─────────────────┤
│ 单元测试 │ ← 大量,快速反馈
│ (函数/方法) │
└─────────────────┘

按测试类型分类

  • 功能测试用例:验证功能是否正确实现
  • 性能测试用例:验证系统性能指标
  • 安全测试用例:验证系统安全性
  • 兼容性测试用例:验证跨平台兼容性
  • 易用性测试用例:验证用户体验

二、测试用例设计方法

2.1 等价类划分法

基本原理

将输入域划分为若干等价类,从每个等价类中选取代表性数据进行测试。

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用户年龄验证功能测试
function validateAge(age) {
if (age < 0) return "年龄不能为负数";
if (age < 18) return "未成年用户";
if (age <= 65) return "成年用户";
if (age > 65) return "老年用户";
if (age > 150) return "年龄超出合理范围";
}

// 等价类划分
const testCases = [
// 有效等价类
{ input: 5, expected: "未成年用户", description: "有效年龄-儿童" },
{ input: 25, expected: "成年用户", description: "有效年龄-成年" },
{ input: 70, expected: "老年用户", description: "有效年龄-老年" },

// 无效等价类
{ input: -5, expected: "年龄不能为负数", description: "无效年龄-负数" },
{ input: 200, expected: "年龄超出合理范围", description: "无效年龄-超大值" },
{ input: "abc", expected: "输入格式错误", description: "无效年龄-非数字" }
];

2.2 边界值分析法

基本原理

重点测试输入域边界上和边界附近的值,因为边界处最容易出现错误。

边界值测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def test_password_length_validation():
"""密码长度验证的边界值测试"""

# 边界值:最小长度8,最大长度20
boundary_test_cases = [
# 下边界测试
("1234567", False, "7位密码-低于最小长度"),
("12345678", True, "8位密码-最小长度边界"),
("123456789", True, "9位密码-最小长度+1"),

# 上边界测试
("12345678901234567890", True, "20位密码-最大长度边界"),
("123456789012345678901", False, "21位密码-超过最大长度"),
("1234567890123456789", True, "19位密码-最大长度-1"),

# 特殊边界
("", False, "空密码"),
(None, False, "空值密码")
]

for password, expected, description in boundary_test_cases:
result = validate_password_length(password)
assert result == expected, f"测试失败: {description}"

2.3 决策表法

适用场景

当系统行为依赖于多个条件的组合时,使用决策表法能够系统性地覆盖所有可能的条件组合。

决策表示例

1
2
3
4
5
6
7
8
9
10
用户权限验证决策表:

条件\规则 | R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8
------------|----|----|----|----|----|----|----|----|
用户已登录 | Y | Y | Y | Y | N | N | N | N
用户已激活 | Y | Y | N | N | Y | Y | N | N
有访问权限 | Y | N | Y | N | Y | N | Y | N
------------|----|----|----|----|----|----|----|----|
允许访问 | Y | N | N | N | N | N | N | N
显示错误信息 | N | Y | Y | Y | Y | Y | Y | Y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基于决策表的测试用例
const permissionTestCases = [
{
name: "R1-完全授权用户",
conditions: { loggedIn: true, activated: true, hasPermission: true },
expected: { allowAccess: true, showError: false }
},
{
name: "R2-无权限的已激活用户",
conditions: { loggedIn: true, activated: true, hasPermission: false },
expected: { allowAccess: false, showError: true }
},
{
name: "R5-未登录但有权限用户",
conditions: { loggedIn: false, activated: true, hasPermission: true },
expected: { allowAccess: false, showError: true }
}
// ... 其他规则
];

2.4 状态转换测试

状态图建模

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 订单状态转换测试
class OrderStateMachine {
constructor() {
this.state = 'CREATED';
this.validTransitions = {
'CREATED': ['PAID', 'CANCELLED'],
'PAID': ['SHIPPED', 'REFUNDED'],
'SHIPPED': ['DELIVERED', 'RETURNED'],
'DELIVERED': ['COMPLETED'],
'CANCELLED': [],
'REFUNDED': [],
'RETURNED': ['REFUNDED'],
'COMPLETED': []
};
}

transition(newState) {
if (this.validTransitions[this.state].includes(newState)) {
this.state = newState;
return true;
}
return false;
}
}

// 状态转换测试用例
const stateTransitionTests = [
{
name: "正常订单流程",
transitions: ['CREATED', 'PAID', 'SHIPPED', 'DELIVERED', 'COMPLETED'],
expected: true
},
{
name: "订单取消流程",
transitions: ['CREATED', 'CANCELLED'],
expected: true
},
{
name: "无效状态转换",
transitions: ['CREATED', 'SHIPPED'], // 跳过支付直接发货
expected: false
}
];

三、不同类型的测试用例

3.1 单元测试用例

函数级测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import unittest
from unittest.mock import patch, Mock

class TestCalculator(unittest.TestCase):

def setUp(self):
"""测试前置条件"""
self.calculator = Calculator()

def test_add_positive_numbers(self):
"""测试正数相加"""
result = self.calculator.add(2, 3)
self.assertEqual(result, 5)

def test_add_negative_numbers(self):
"""测试负数相加"""
result = self.calculator.add(-2, -3)
self.assertEqual(result, -5)

def test_divide_by_zero(self):
"""测试除零异常"""
with self.assertRaises(ZeroDivisionError):
self.calculator.divide(10, 0)

@patch('requests.get')
def test_api_call_with_mock(self, mock_get):
"""测试API调用(使用Mock)"""
# 设置Mock返回值
mock_response = Mock()
mock_response.json.return_value = {'result': 'success'}
mock_response.status_code = 200
mock_get.return_value = mock_response

# 执行测试
result = self.calculator.fetch_data('http://api.example.com')

# 验证结果
self.assertEqual(result['result'], 'success')
mock_get.assert_called_once_with('http://api.example.com')

3.2 集成测试用例

API集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Jest + Supertest API测试
const request = require('supertest');
const app = require('../app');

describe('User API Integration Tests', () => {
let authToken;
let userId;

beforeAll(async () => {
// 测试前置:创建测试用户并获取认证token
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'testpassword'
});

authToken = response.body.token;
});

describe('POST /api/users', () => {
it('should create a new user with valid data', async () => {
const userData = {
name: 'Test User',
email: 'newuser@example.com',
password: 'password123'
};

const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send(userData)
.expect(201);

expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
expect(response.body.email).toBe(userData.email);
expect(response.body).not.toHaveProperty('password');

userId = response.body.id;
});

it('should return 400 for invalid email format', async () => {
const invalidData = {
name: 'Test User',
email: 'invalid-email',
password: 'password123'
};

const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send(invalidData)
.expect(400);

expect(response.body).toHaveProperty('error');
expect(response.body.error).toContain('email');
});
});

describe('GET /api/users/:id', () => {
it('should retrieve user by id', async () => {
const response = await request(app)
.get(`/api/users/${userId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);

expect(response.body.id).toBe(userId);
expect(response.body).toHaveProperty('name');
expect(response.body).toHaveProperty('email');
});

it('should return 404 for non-existent user', async () => {
await request(app)
.get('/api/users/999999')
.set('Authorization', `Bearer ${authToken}`)
.expect(404);
});
});

afterAll(async () => {
// 测试后清理:删除测试数据
if (userId) {
await request(app)
.delete(`/api/users/${userId}`)
.set('Authorization', `Bearer ${authToken}`);
}
});
});

3.3 端到端测试用例

Web应用E2E测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// Cypress E2E测试
describe('E-commerce User Journey', () => {
beforeEach(() => {
// 访问首页
cy.visit('/');

// 清除购物车
cy.clearLocalStorage();
cy.clearCookies();
});

it('完整的购买流程', () => {
// 1. 用户注册/登录
cy.get('[data-testid="login-button"]').click();
cy.get('[data-testid="email-input"]').type('test@example.com');
cy.get('[data-testid="password-input"]').type('password123');
cy.get('[data-testid="submit-button"]').click();

// 验证登录成功
cy.get('[data-testid="user-menu"]').should('be.visible');
cy.get('[data-testid="user-name"]').should('contain', 'Test User');

// 2. 浏览商品
cy.get('[data-testid="product-category"]').first().click();
cy.get('[data-testid="product-list"]').should('be.visible');

// 3. 添加商品到购物车
cy.get('[data-testid="product-item"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();

// 验证添加成功
cy.get('[data-testid="cart-count"]').should('contain', '1');
cy.get('[data-testid="success-message"]').should('be.visible');

// 4. 查看购物车
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="cart-items"]').should('have.length', 1);

// 5. 结算流程
cy.get('[data-testid="checkout-button"]').click();

// 填写配送信息
cy.get('[data-testid="shipping-address"]').type('123 Test Street');
cy.get('[data-testid="shipping-city"]').type('Test City');
cy.get('[data-testid="shipping-zip"]').type('12345');

// 选择支付方式
cy.get('[data-testid="payment-method-card"]').click();
cy.get('[data-testid="card-number"]').type('4111111111111111');
cy.get('[data-testid="card-expiry"]').type('12/25');
cy.get('[data-testid="card-cvv"]').type('123');

// 6. 完成订单
cy.get('[data-testid="place-order"]').click();

// 验证订单成功
cy.get('[data-testid="order-confirmation"]').should('be.visible');
cy.get('[data-testid="order-number"]').should('exist');

// 7. 查看订单历史
cy.get('[data-testid="user-menu"]').click();
cy.get('[data-testid="order-history"]').click();
cy.get('[data-testid="order-list"]').should('contain', '订单');
});

it('购物车功能测试', () => {
// 登录
cy.login('test@example.com', 'password123');

// 添加多个商品
cy.addProductToCart('product-1');
cy.addProductToCart('product-2');

// 验证购物车数量
cy.get('[data-testid="cart-count"]').should('contain', '2');

// 修改商品数量
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="quantity-input"]').first().clear().type('3');
cy.get('[data-testid="update-quantity"]').first().click();

// 验证总价更新
cy.get('[data-testid="cart-total"]').should('not.contain', '0');

// 删除商品
cy.get('[data-testid="remove-item"]').first().click();
cy.get('[data-testid="confirm-remove"]').click();

// 验证商品已删除
cy.get('[data-testid="cart-items"]').should('have.length', 1);
});
});

四、测试用例管理

4.1 测试用例组织结构

分层组织方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
测试用例目录结构:
project-tests/
├── unit-tests/ # 单元测试
│ ├── models/
│ ├── services/
│ └── utils/
├── integration-tests/ # 集成测试
│ ├── api/
│ ├── database/
│ └── external-services/
├── e2e-tests/ # 端到端测试
│ ├── user-journeys/
│ ├── admin-workflows/
│ └── mobile-scenarios/
├── performance-tests/ # 性能测试
│ ├── load-tests/
│ ├── stress-tests/
│ └── spike-tests/
└── security-tests/ # 安全测试
├── authentication/
├── authorization/
└── data-validation/

4.2 测试用例标识与命名

命名规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 测试用例命名规范
const testCaseNamingConvention = {
// 格式:[模块]_[功能]_[场景]_[预期结果]
examples: [
"USER_LOGIN_VALID_CREDENTIALS_SUCCESS",
"USER_LOGIN_INVALID_PASSWORD_FAILURE",
"CART_ADD_ITEM_DUPLICATE_PRODUCT_UPDATE_QUANTITY",
"PAYMENT_PROCESS_EXPIRED_CARD_ERROR_MESSAGE",
"ADMIN_USER_DELETE_ACTIVE_USER_CONFIRMATION_REQUIRED"
],

// 编号规范
numbering: {
format: "TC_[模块代码]_[序号]",
examples: [
"TC_USER_001", // 用户模块第1个测试用例
"TC_CART_015", // 购物车模块第15个测试用例
"TC_PAY_008" // 支付模块第8个测试用例
]
}
};

4.3 测试数据管理

测试数据分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 测试数据管理策略
const testDataManagement = {
// 静态测试数据
staticData: {
validUsers: [
{ id: 1, email: "admin@test.com", role: "admin", status: "active" },
{ id: 2, email: "user@test.com", role: "user", status: "active" },
{ id: 3, email: "guest@test.com", role: "guest", status: "inactive" }
],
products: [
{ id: 101, name: "Laptop", price: 999.99, category: "Electronics" },
{ id: 102, name: "Book", price: 29.99, category: "Education" }
]
},

// 动态测试数据生成
dynamicData: {
generateUser: () => ({
email: `test_${Date.now()}@example.com`,
password: "TestPass123!",
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
phone: faker.phone.phoneNumber()
}),

generateOrder: (userId) => ({
userId: userId,
orderDate: new Date().toISOString(),
items: [
{ productId: 101, quantity: 2, price: 999.99 },
{ productId: 102, quantity: 1, price: 29.99 }
],
total: 2029.97
})
},

// 边界值数据
boundaryData: {
strings: {
empty: "",
single: "a",
maxLength: "a".repeat(255),
overMaxLength: "a".repeat(256),
specialChars: "!@#$%^&*()_+-=[]{}|;:,.<>?",
unicode: "测试数据🚀",
sql: "'; DROP TABLE users; --"
},
numbers: {
zero: 0,
negative: -1,
maxInt: 2147483647,
minInt: -2147483648,
decimal: 123.456,
scientific: 1.23e10
}
}
};

测试数据工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Python测试数据工厂
import factory
from faker import Faker
from datetime import datetime, timedelta

fake = Faker('zh_CN')

class UserFactory(factory.Factory):
class Meta:
model = User

email = factory.LazyAttribute(lambda obj: f"test_{fake.random_int()}@example.com")
username = factory.LazyAttribute(lambda obj: fake.user_name())
first_name = factory.LazyAttribute(lambda obj: fake.first_name())
last_name = factory.LazyAttribute(lambda obj: fake.last_name())
phone = factory.LazyAttribute(lambda obj: fake.phone_number())
birth_date = factory.LazyAttribute(lambda obj: fake.date_of_birth(minimum_age=18, maximum_age=80))
is_active = True
created_at = factory.LazyFunction(datetime.now)

class ProductFactory(factory.Factory):
class Meta:
model = Product

name = factory.LazyAttribute(lambda obj: fake.catch_phrase())
description = factory.LazyAttribute(lambda obj: fake.text(max_nb_chars=200))
price = factory.LazyAttribute(lambda obj: fake.pydecimal(left_digits=3, right_digits=2, positive=True))
category = factory.Iterator(['Electronics', 'Books', 'Clothing', 'Home'])
stock_quantity = factory.LazyAttribute(lambda obj: fake.random_int(min=0, max=1000))

class OrderFactory(factory.Factory):
class Meta:
model = Order

user = factory.SubFactory(UserFactory)
order_date = factory.LazyFunction(datetime.now)
status = factory.Iterator(['pending', 'processing', 'shipped', 'delivered', 'cancelled'])
total_amount = factory.LazyAttribute(lambda obj: fake.pydecimal(left_digits=4, right_digits=2, positive=True))

# 使用工厂创建测试数据
def test_user_creation():
# 创建单个用户
user = UserFactory()
assert user.email is not None
assert user.is_active is True

# 批量创建用户
users = UserFactory.create_batch(10)
assert len(users) == 10

# 创建特定属性的用户
admin_user = UserFactory(email="admin@test.com", is_active=True)
assert admin_user.email == "admin@test.com"

五、自动化测试用例

5.1 测试框架选择

不同语言的测试框架对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// JavaScript测试框架对比
const testFrameworks = {
jest: {
pros: ["零配置", "内置断言库", "快照测试", "代码覆盖率"],
cons: ["主要针对React", "配置复杂场景困难"],
bestFor: "React应用、Node.js项目"
},

mocha: {
pros: ["灵活配置", "丰富的插件", "支持多种断言库"],
cons: ["需要额外配置", "学习曲线陡峭"],
bestFor: "复杂项目、需要自定义配置"
},

cypress: {
pros: ["真实浏览器环境", "时间旅行调试", "自动等待"],
cons: ["只支持Chrome系", "不支持多标签页"],
bestFor: "E2E测试、前端集成测试"
},

playwright: {
pros: ["多浏览器支持", "并行执行", "移动端测试"],
cons: ["相对较新", "学习成本"],
bestFor: "跨浏览器E2E测试"
}
};

5.2 Page Object模式

页面对象设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Page Object Model 实现
class LoginPage {
constructor(page) {
this.page = page;

// 页面元素定位器
this.selectors = {
emailInput: '[data-testid="email-input"]',
passwordInput: '[data-testid="password-input"]',
loginButton: '[data-testid="login-button"]',
errorMessage: '[data-testid="error-message"]',
forgotPasswordLink: '[data-testid="forgot-password"]'
};
}

// 页面操作方法
async navigate() {
await this.page.goto('/login');
await this.page.waitForSelector(this.selectors.emailInput);
}

async fillEmail(email) {
await this.page.fill(this.selectors.emailInput, email);
}

async fillPassword(password) {
await this.page.fill(this.selectors.passwordInput, password);
}

async clickLogin() {
await this.page.click(this.selectors.loginButton);
}

async login(email, password) {
await this.fillEmail(email);
await this.fillPassword(password);
await this.clickLogin();
}

// 验证方法
async getErrorMessage() {
return await this.page.textContent(this.selectors.errorMessage);
}

async isLoginButtonEnabled() {
return await this.page.isEnabled(this.selectors.loginButton);
}

async waitForRedirect() {
await this.page.waitForURL('/dashboard');
}
}

// 使用Page Object的测试用例
describe('Login Functionality', () => {
let loginPage;

beforeEach(async () => {
loginPage = new LoginPage(page);
await loginPage.navigate();
});

test('successful login with valid credentials', async () => {
await loginPage.login('user@example.com', 'password123');
await loginPage.waitForRedirect();

expect(page.url()).toContain('/dashboard');
});

test('login failure with invalid credentials', async () => {
await loginPage.login('user@example.com', 'wrongpassword');

const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain('Invalid credentials');
});

test('login button disabled with empty fields', async () => {
const isEnabled = await loginPage.isLoginButtonEnabled();
expect(isEnabled).toBe(false);
});
});

5.3 数据驱动测试

参数化测试实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# pytest参数化测试
import pytest

class TestUserValidation:

@pytest.mark.parametrize("email,password,expected_result,test_description", [
("valid@example.com", "ValidPass123!", True, "有效邮箱和密码"),
("invalid-email", "ValidPass123!", False, "无效邮箱格式"),
("valid@example.com", "weak", False, "弱密码"),
("", "ValidPass123!", False, "空邮箱"),
("valid@example.com", "", False, "空密码"),
("test@test.com", "Test123!", True, "边界有效数据"),
("a" * 100 + "@example.com", "ValidPass123!", False, "超长邮箱"),
("valid@example.com", "a" * 200, False, "超长密码")
])
def test_user_registration_validation(self, email, password, expected_result, test_description):
"""参数化测试用户注册验证"""
result = validate_user_registration(email, password)
assert result == expected_result, f"测试失败: {test_description}"

@pytest.mark.parametrize("age,expected_category", [
(5, "child"),
(17, "teenager"),
(18, "adult"),
(65, "adult"),
(66, "senior"),
(0, "infant"),
(-1, "invalid"),
(150, "invalid")
])
def test_age_categorization(self, age, expected_category):
"""年龄分类测试"""
result = categorize_by_age(age)
assert result == expected_category

CSV数据驱动测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Jest + CSV数据驱动测试
const fs = require('fs');
const csv = require('csv-parser');

describe('Data-driven Login Tests', () => {
let testData = [];

beforeAll((done) => {
fs.createReadStream('test-data/login-test-data.csv')
.pipe(csv())
.on('data', (row) => {
testData.push(row);
})
.on('end', () => {
done();
});
});

test.each(testData)('Login test: $description', async (data) => {
const { email, password, expectedResult, description } = data;

const result = await loginUser(email, password);

if (expectedResult === 'success') {
expect(result.success).toBe(true);
expect(result.token).toBeDefined();
} else {
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
}
});
});

六、性能测试用例

6.1 负载测试用例

JMeter测试计划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- JMeter测试计划示例 -->
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan testname="API Load Test" enabled="true">
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel">
<collectionProp name="Arguments.arguments">
<elementProp name="baseUrl" elementType="Argument">
<stringProp name="Argument.name">baseUrl</stringProp>
<stringProp name="Argument.value">https://api.example.com</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>

<hashTree>
<ThreadGroup testname="User Load" enabled="true">
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
</ThreadGroup>

<hashTree>
<HTTPSamplerProxy testname="Login API" enabled="true">
<stringProp name="HTTPSampler.domain">${baseUrl}</stringProp>
<stringProp name="HTTPSampler.path">/api/auth/login</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>

<ResponseAssertion testname="Response Code Assertion" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
</ResponseAssertion>

<ResponseAssertion testname="Response Time Assertion" enabled="true">
<stringProp name="Assertion.test_field">Assertion.response_time</stringProp>
<stringProp name="Assertion.duration">2000</stringProp>
</ResponseAssertion>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>

K6性能测试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// K6负载测试脚本
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

// 自定义指标
const errorRate = new Rate('errors');

// 测试配置
export let options = {
stages: [
{ duration: '2m', target: 10 }, // 预热阶段
{ duration: '5m', target: 50 }, // 正常负载
{ duration: '2m', target: 100 }, // 峰值负载
{ duration: '5m', target: 100 }, // 持续峰值
{ duration: '2m', target: 0 }, // 降负载
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95%的请求响应时间小于2秒
http_req_failed: ['rate<0.1'], // 错误率小于10%
errors: ['rate<0.1'], // 自定义错误率小于10%
},
};

// 测试数据
const users = [
{ email: 'user1@example.com', password: 'password123' },
{ email: 'user2@example.com', password: 'password123' },
{ email: 'user3@example.com', password: 'password123' },
];

export default function() {
// 随机选择用户
const user = users[Math.floor(Math.random() * users.length)];

// 1. 登录请求
const loginResponse = http.post('https://api.example.com/auth/login', {
email: user.email,
password: user.password
}, {
headers: { 'Content-Type': 'application/json' }
});

// 验证登录响应
const loginSuccess = check(loginResponse, {
'login status is 200': (r) => r.status === 200,
'login response time < 1000ms': (r) => r.timings.duration < 1000,
'login returns token': (r) => r.json('token') !== undefined,
});

errorRate.add(!loginSuccess);

if (loginSuccess) {
const token = loginResponse.json('token');

// 2. 获取用户信息
const profileResponse = http.get('https://api.example.com/user/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});

check(profileResponse, {
'profile status is 200': (r) => r.status === 200,
'profile response time < 500ms': (r) => r.timings.duration < 500,
});

// 3. 获取数据列表
const dataResponse = http.get('https://api.example.com/data?page=1&limit=20', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});

check(dataResponse, {
'data status is 200': (r) => r.status === 200,
'data response time < 1500ms': (r) => r.timings.duration < 1500,
'data contains items': (r) => r.json('items').length > 0,
});
}

// 模拟用户思考时间
sleep(Math.random() * 3 + 1); // 1-4秒随机等待
}

// 测试结束后的清理函数
export function teardown(data) {
console.log('Performance test completed');
}

七、测试用例最佳实践

7.1 测试用例设计原则

FIRST原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// FIRST原则示例
const testPrinciples = {
Fast: {
description: "测试应该快速执行",
example: `
// ✅ 好的实践:使用Mock避免网络调用
test('calculate total price', () => {
const mockProducts = [
{ price: 10.99, quantity: 2 },
{ price: 5.99, quantity: 1 }
];
const total = calculateTotal(mockProducts);
expect(total).toBe(27.97);
});

// ❌ 避免:实际的网络请求
test('fetch products from API', async () => {
const products = await fetchProductsFromAPI(); // 慢
expect(products.length).toBeGreaterThan(0);
});`
},

Independent: {
description: "测试之间应该相互独立",
example: `
// ✅ 好的实践:每个测试独立设置数据
describe('User Service', () => {
beforeEach(() => {
// 每个测试前重置状态
userService.reset();
});

test('create user', () => {
const user = userService.create({ name: 'John' });
expect(user.id).toBeDefined();
});

test('delete user', () => {
const user = userService.create({ name: 'Jane' });
const result = userService.delete(user.id);
expect(result).toBe(true);
});
});`
},

Repeatable: {
description: "测试结果应该可重复",
example: `
// ✅ 好的实践:使用固定的测试数据
test('format date', () => {
const fixedDate = new Date('2025-07-18T10:00:00Z');
const formatted = formatDate(fixedDate);
expect(formatted).toBe('2025-07-18');
});

// ❌ 避免:依赖当前时间
test('is recent date', () => {
const now = new Date(); // 不可预测
const result = isRecent(now);
expect(result).toBe(true);
});`
},

SelfValidating: {
description: "测试应该有明确的通过/失败结果",
example: `
// ✅ 好的实践:明确的断言
test('user validation', () => {
const result = validateUser({ email: 'test@example.com' });
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});

// ❌ 避免:需要人工判断的输出
test('print user info', () => {
const user = getUser(1);
console.log(user); // 需要人工检查
});`
},

Timely: {
description: "测试应该及时编写",
example: `
// ✅ 好的实践:TDD - 先写测试
test('calculate discount', () => {
// 先写测试,定义期望行为
const discount = calculateDiscount(100, 'VIP');
expect(discount).toBe(20);
});

// 然后实现功能
function calculateDiscount(amount, customerType) {
if (customerType === 'VIP') {
return amount * 0.2;
}
return 0;
}`
}
};

7.2 测试用例维护策略

测试用例生命周期管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 测试用例维护策略
const testMaintenanceStrategy = {
// 1. 定期审查
regularReview: {
frequency: "每月一次",
activities: [
"检查失效的测试用例",
"更新过时的测试数据",
"优化执行缓慢的测试",
"删除重复的测试用例"
]
},

// 2. 版本控制
versionControl: {
strategy: "与代码同步版本控制",
practices: [
"测试用例与功能代码一起提交",
"使用分支管理测试用例变更",
"代码审查包含测试用例审查",
"维护测试用例变更日志"
]
},

// 3. 重构策略
refactoringStrategy: {
triggers: [
"测试执行时间过长",
"测试用例重复度高",
"维护成本过高",
"业务逻辑变更"
],
methods: [
"提取公共测试方法",
"使用测试工厂模式",
"实现页面对象模式",
"优化测试数据管理"
]
}
};

// 测试用例重构示例
class TestRefactoringExample {
// 重构前:重复的测试代码
testUserCreationBefore() {
// 重复的设置代码
const userData = {
email: 'test@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe'
};

const user = createUser(userData);
expect(user.id).toBeDefined();
expect(user.email).toBe(userData.email);
}

// 重构后:提取公共方法
createTestUser(overrides = {}) {
const defaultUserData = {
email: 'test@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe'
};

return { ...defaultUserData, ...overrides };
}

testUserCreationAfter() {
const userData = this.createTestUser();
const user = createUser(userData);

expect(user.id).toBeDefined();
expect(user.email).toBe(userData.email);
}

testUserCreationWithCustomEmail() {
const userData = this.createTestUser({
email: 'custom@example.com'
});
const user = createUser(userData);

expect(user.email).toBe('custom@example.com');
}
}

7.3 测试报告与度量

测试覆盖率分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Jest覆盖率配置
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/utils/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/serviceWorker.js',
'!src/**/*.test.js'
]
};

// 自定义测试报告生成
class TestReportGenerator {
generateReport(testResults) {
const report = {
summary: {
totalTests: testResults.length,
passed: testResults.filter(t => t.status === 'passed').length,
failed: testResults.filter(t => t.status === 'failed').length,
skipped: testResults.filter(t => t.status === 'skipped').length,
executionTime: testResults.reduce((sum, t) => sum + t.duration, 0)
},

coverage: {
statements: this.calculateCoverage('statements'),
branches: this.calculateCoverage('branches'),
functions: this.calculateCoverage('functions'),
lines: this.calculateCoverage('lines')
},

failedTests: testResults
.filter(t => t.status === 'failed')
.map(t => ({
name: t.name,
error: t.error,
file: t.file,
line: t.line
})),

performance: {
slowestTests: testResults
.sort((a, b) => b.duration - a.duration)
.slice(0, 10),
averageExecutionTime: testResults.reduce((sum, t) => sum + t.duration, 0) / testResults.length
}
};

return report;
}

generateHTMLReport(report) {
return `
<!DOCTYPE html>
<html>
<head>
<title>测试报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
.passed { color: green; }
.failed { color: red; }
.coverage { margin: 20px 0; }
.coverage-bar { width: 200px; height: 20px; background: #ddd; border-radius: 10px; }
.coverage-fill { height: 100%; background: #4caf50; border-radius: 10px; }
</style>
</head>
<body>
<h1>测试执行报告</h1>

<div class="summary">
<h2>测试概要</h2>
<p>总测试数: ${report.summary.totalTests}</p>
<p class="passed">通过: ${report.summary.passed}</p>
<p class="failed">失败: ${report.summary.failed}</p>
<p>跳过: ${report.summary.skipped}</p>
<p>执行时间: ${report.summary.executionTime}ms</p>
</div>

<div class="coverage">
<h2>代码覆盖率</h2>
<div>
<span>语句覆盖率: ${report.coverage.statements}%</span>
<div class="coverage-bar">
<div class="coverage-fill" style="width: ${report.coverage.statements}%"></div>
</div>
</div>
<!-- 其他覆盖率指标... -->
</div>

${report.failedTests.length > 0 ? `
<div class="failed-tests">
<h2>失败的测试</h2>
<ul>
${report.failedTests.map(test => `
<li>
<strong>${test.name}</strong><br>
<span style="color: red;">${test.error}</span><br>
<small>${test.file}:${test.line}</small>
</li>
`).join('')}
</ul>
</div>
` : ''}
</body>
</html>
`;
}
}

八、总结与展望

8.1 测试用例设计核心要点

  1. 系统性设计:使用科学的测试设计方法,确保测试覆盖全面
  2. 质量优先:编写高质量、可维护的测试用例
  3. 自动化驱动:优先考虑自动化执行和持续集成
  4. 数据驱动:合理管理测试数据,提高测试效率
  5. 持续改进:定期审查和优化测试用例

8.2 未来发展趋势

  • AI辅助测试:利用人工智能生成和优化测试用例
  • 智能化测试:自动识别测试场景和生成测试数据
  • 云端测试:基于云平台的大规模并行测试执行
  • 可视化测试:图形化的测试用例设计和管理工具

测试用例是软件质量保证的基石,掌握测试用例的设计和编写技能对于任何软件开发团队都至关重要。通过系统的学习和实践,我们能够构建更加可靠、高效的测试体系,确保软件产品的质量和用户体验。

希望这份测试用例指南能够帮助你在软件测试的道路上更进一步!🧪✨