测试驱动开发教程 — TDD实战

适用人群:所有开发者
学习时长:约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%覆盖
✅ 边界测试:测试边界条件
✅ 异常测试:测试错误处理


学习建议

  1. 先理解TDD流程:Red → Green → Refactor
  2. 从简单的单元测试开始
  3. 测试核心业务逻辑
  4. 使用Mock隔离依赖
  5. 建立测试覆盖率目标

下一步学习

返回首页