适用人群:所有开发者
学习时长:约1-2天
工具:PHPUnit / Jest / pytest / Go testing
重要程度:★★★★☆(高质量代码必备)
一、什么是TDD?
TDD(Test-Driven Development)是一种开发方法,先写测试再写代码。
| 步骤 | 说明 |
|---|---|
| Red | 先写失败的测试 |
| Green | 写最少代码让测试通过 |
| Refactor | 重构代码,保持测试通过 |
二、测试类型
| 类型 | 说明 | 工具 |
|---|---|---|
| 单元测试 | 测试单个函数/类 | PHPUnit/Jest/pytest |
| 集成测试 | 测试多个组件协作 | PHPUnit/Jest/pytest |
| E2E测试 | 测试完整流程 | Cypress/Playwright |
三、PHP测试(PHPUnit)
3.1 安装配置
# 安装
composer require --dev phpunit/phpunit
# 运行测试
./vendor/bin/phpunit
3.2 编写测试
// tests/Unit/UserTest.php
use PHPUnit\Framework\TestCase;
use App\Models\User;
class UserTest extends TestCase
{
// 测试创建用户
public function test_create_user()
{
$user = new User('张三', 'zhangsan@example.com');
$this->assertEquals('张三', $user->getName());
$this->assertEquals('zhangsan@example.com', $user->getEmail());
}
// 测试验证
public function test_invalid_email()
{
$this->expectException(\InvalidArgumentException::class);
new User('张三', 'invalid-email');
}
// 测试密码加密
public function test_password_hash()
{
$user = new User('张三', 'zhangsan@example.com');
$user->setPassword('123456');
$this->assertTrue(password_verify('123456', $user->getPassword()));
$this->assertFalse(password_verify('wrong', $user->getPassword()));
}
}
// tests/Feature/UserApiTest.php
class UserApiTest extends TestCase
{
use RefreshDatabase;
// 测试注册接口
public function test_register()
{
$response = $this->postJson('/api/auth/register', [
'username' => 'testuser',
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertStatus(201)
->assertJsonStructure(['data' => ['user', 'token']]);
$this->assertDatabaseHas('users', [
'username' => 'testuser',
'email' => 'test@example.com',
]);
}
// 测试登录接口
public function test_login()
{
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);
$response = $this->postJson('/api/auth/login', [
'email' => $user->email,
'password' => 'password123',
]);
$response->assertStatus(200)
->assertJsonStructure(['data' => ['user', 'token']]);
}
}
四、JavaScript测试(Jest)
4.1 安装配置
# 安装
npm install -D jest @types/jest
# 运行测试
npm test
4.2 编写测试
// utils/math.test.js
const { add, multiply, divide } = require('./math')
describe('Math Utils', () => {
test('add', () => {
expect(add(1, 2)).toBe(3)
expect(add(-1, 1)).toBe(0)
})
test('multiply', () => {
expect(multiply(2, 3)).toBe(6)
expect(multiply(0, 5)).toBe(0)
})
test('divide', () => {
expect(divide(10, 2)).toBe(5)
expect(divide(10, 3)).toBeCloseTo(3.33, 2)
})
test('divide by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero')
})
})
// services/user.test.js
const UserService = require('./user')
describe('UserService', () => {
let userService
beforeEach(() => {
userService = new UserService()
})
test('create user', async () => {
const user = await userService.create({
username: 'testuser',
email: 'test@example.com',
password: '123456'
})
expect(user).toHaveProperty('id')
expect(user.username).toBe('testuser')
})
test('find user by id', async () => {
const created = await userService.create({
username: 'testuser',
email: 'test@example.com'
})
const found = await userService.findById(created.id)
expect(found.username).toBe('testuser')
})
test('user not found', async () => {
const user = await userService.findById(999)
expect(user).toBeNull()
})
})
五、Python测试(pytest)
5.1 安装配置
# 安装
pip install pytest pytest-cov
# 运行测试
pytest
pytest --cov=app
5.2 编写测试
# tests/test_user.py
import pytest
from app.models.user import User
from app.services.user_service import UserService
class TestUser:
def test_create_user(self):
user = User(username='testuser', email='test@example.com')
assert user.username == 'testuser'
assert user.email == 'test@example.com'
def test_invalid_email(self):
with pytest.raises(ValueError):
User(username='testuser', email='invalid')
def test_password_hash(self):
user = User(username='testuser', email='test@example.com')
user.set_password('123456')
assert user.check_password('123456') == True
assert user.check_password('wrong') == False
class TestUserService:
@pytest.fixture
def service(self):
return UserService()
def test_create_user(self, service):
user = service.create({
'username': 'testuser',
'email': 'test@example.com',
'password': '123456'
})
assert user.id is not None
assert user.username == 'testuser'
def test_find_user(self, service):
user = service.create({
'username': 'testuser',
'email': 'test@example.com'
})
found = service.find_by_id(user.id)
assert found.username == 'testuser'
# tests/test_api.py
class TestUserAPI:
def test_register(self, client):
response = client.post('/api/auth/register', json={
'username': 'testuser',
'email': 'test@example.com',
'password': '123456'
})
assert response.status_code == 201
assert 'token' in response.json['data']
def test_login(self, client, user):
response = client.post('/api/auth/login', json={
'email': user.email,
'password': '123456'
})
assert response.status_code == 200
六、Go测试
// user_test.go
package user
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateUser(t *testing.T) {
user := NewUser("testuser", "test@example.com")
assert.Equal(t, "testuser", user.Username)
assert.Equal(t, "test@example.com", user.Email)
}
func TestInvalidEmail(t *testing.T) {
_, err := NewUser("testuser", "invalid")
assert.Error(t, err)
}
func TestPasswordHash(t *testing.T) {
user, _ := NewUser("testuser", "test@example.com")
user.SetPassword("123456")
assert.True(t, user.CheckPassword("123456"))
assert.False(t, user.CheckPassword("wrong"))
}
// 表驱动测试
func TestValidateAge(t *testing.T) {
tests := []struct {
name string
age int
wantErr bool
}{
{"valid age", 25, false},
{"zero age", 0, false},
{"negative age", -1, true},
{"too old", 200, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateAge(tt.age)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
七、测试最佳实践
✅ 测试命名清晰:test_功能_场景_预期结果
✅ 每个测试只测一个功能
✅ 测试独立:不依赖其他测试
✅ 测试可重复:多次运行结果相同
✅ 测试快速:单元测试应该很快
✅ 测试覆盖率:核心逻辑100%覆盖
✅ 边界测试:测试边界条件
✅ 异常测试:测试错误处理
学习建议
- 先理解TDD流程:Red → Green → Refactor
- 从简单的单元测试开始
- 测试核心业务逻辑
- 使用Mock隔离依赖
- 建立测试覆盖率目标