Skip to content

测试用例编写

📋 概述

测试用例编写是软件测试的核心技能,直接影响测试的有效性和代码质量。好的测试用例应该清晰、可维护、全面覆盖功能需求,并能快速定位问题。本文档将系统介绍如何在Node.js项目中编写高质量的测试用例。

🎯 学习目标

  • 掌握测试用例设计的基本原则和方法
  • 学会编写清晰、可维护的测试代码
  • 了解不同类型功能的测试策略
  • 掌握测试数据管理和边界条件测试

📚 测试用例设计原则

FIRST原则

javascript
const FIRSTPrinciples = {
  FAST: {
    principle: '快速执行',
    description: '测试应该能够快速运行,理想情况下毫秒级完成',
    example: '单元测试应避免网络调用、文件IO等耗时操作'
  },
  
  ISOLATED: {
    principle: '独立隔离',
    description: '测试之间不应相互依赖,可以任意顺序执行',
    example: '每个测试前重置状态,使用独立的测试数据'
  },
  
  REPEATABLE: {
    principle: '可重复执行',
    description: '在任何环境下运行多次都能得到相同结果',
    example: '避免依赖系统时间、随机数等不确定因素'
  },
  
  SELF_VALIDATING: {
    principle: '自我验证',
    description: '测试结果明确,不需要人工判断成功或失败',
    example: '使用断言明确验证期望结果'
  },
  
  TIMELY: {
    principle: '及时编写',
    description: '测试应该在编写功能代码的同时或之前编写',
    example: 'TDD方法或功能完成后立即编写测试'
  }
};

测试用例结构

javascript
// AAA模式:Arrange-Act-Assert
describe('用户服务', () => {
  describe('创建用户功能', () => {
    it('应该成功创建有效用户', () => {
      // Arrange - 准备测试数据和环境
      const userService = new UserService();
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        age: 30
      };
      
      // Act - 执行被测试的操作
      const result = userService.createUser(userData);
      
      // Assert - 验证结果
      expect(result).toEqual(
        expect.objectContaining({
          id: expect.any(String),
          name: userData.name,
          email: userData.email,
          createdAt: expect.any(Date)
        })
      );
    });
  });
});

🔍 不同类型功能的测试策略

纯函数测试

javascript
// math-utils.js
class MathUtils {
  static add(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('Both arguments must be numbers');
    }
    return a + b;
  }
  
  static factorial(n) {
    if (!Number.isInteger(n) || n < 0) {
      throw new Error('Argument must be a non-negative integer');
    }
    if (n === 0 || n === 1) return 1;
    return n * this.factorial(n - 1);
  }
  
  static isPrime(n) {
    if (!Number.isInteger(n) || n < 2) return false;
    for (let i = 2; i <= Math.sqrt(n); i++) {
      if (n % i === 0) return false;
    }
    return true;
  }
}

module.exports = MathUtils;
javascript
// math-utils.test.js
const MathUtils = require('./math-utils');

describe('MathUtils', () => {
  describe('add方法', () => {
    // 正常情况测试
    describe('正常输入', () => {
      it('应该正确计算两个正数的和', () => {
        expect(MathUtils.add(2, 3)).toBe(5);
        expect(MathUtils.add(10, 15)).toBe(25);
      });
      
      it('应该正确处理负数', () => {
        expect(MathUtils.add(-5, 3)).toBe(-2);
        expect(MathUtils.add(-5, -3)).toBe(-8);
        expect(MathUtils.add(5, -3)).toBe(2);
      });
      
      it('应该正确处理零', () => {
        expect(MathUtils.add(0, 5)).toBe(5);
        expect(MathUtils.add(5, 0)).toBe(5);
        expect(MathUtils.add(0, 0)).toBe(0);
      });
      
      it('应该正确处理小数', () => {
        expect(MathUtils.add(0.1, 0.2)).toBeCloseTo(0.3);
        expect(MathUtils.add(1.5, 2.7)).toBeCloseTo(4.2);
      });
      
      it('应该正确处理大数', () => {
        expect(MathUtils.add(Number.MAX_SAFE_INTEGER, 0))
          .toBe(Number.MAX_SAFE_INTEGER);
      });
    });
    
    // 异常情况测试
    describe('异常输入', () => {
      it('应该在参数不是数字时抛出错误', () => {
        expect(() => MathUtils.add('2', 3)).toThrow('Both arguments must be numbers');
        expect(() => MathUtils.add(2, '3')).toThrow('Both arguments must be numbers');
        expect(() => MathUtils.add('a', 'b')).toThrow('Both arguments must be numbers');
      });
      
      it('应该在参数为null或undefined时抛出错误', () => {
        expect(() => MathUtils.add(null, 3)).toThrow();
        expect(() => MathUtils.add(2, undefined)).toThrow();
        expect(() => MathUtils.add(null, undefined)).toThrow();
      });
    });
  });
  
  describe('factorial方法', () => {
    it('应该正确计算阶乘', () => {
      expect(MathUtils.factorial(0)).toBe(1);
      expect(MathUtils.factorial(1)).toBe(1);
      expect(MathUtils.factorial(5)).toBe(120);
      expect(MathUtils.factorial(6)).toBe(720);
    });
    
    it('应该拒绝负数', () => {
      expect(() => MathUtils.factorial(-1)).toThrow();
      expect(() => MathUtils.factorial(-5)).toThrow();
    });
    
    it('应该拒绝非整数', () => {
      expect(() => MathUtils.factorial(1.5)).toThrow();
      expect(() => MathUtils.factorial(3.14)).toThrow();
    });
  });
  
  describe('isPrime方法', () => {
    it('应该正确识别质数', () => {
      expect(MathUtils.isPrime(2)).toBe(true);
      expect(MathUtils.isPrime(3)).toBe(true);
      expect(MathUtils.isPrime(5)).toBe(true);
      expect(MathUtils.isPrime(7)).toBe(true);
      expect(MathUtils.isPrime(11)).toBe(true);
      expect(MathUtils.isPrime(97)).toBe(true);
    });
    
    it('应该正确识别合数', () => {
      expect(MathUtils.isPrime(4)).toBe(false);
      expect(MathUtils.isPrime(6)).toBe(false);
      expect(MathUtils.isPrime(8)).toBe(false);
      expect(MathUtils.isPrime(9)).toBe(false);
      expect(MathUtils.isPrime(100)).toBe(false);
    });
    
    it('应该正确处理边界情况', () => {
      expect(MathUtils.isPrime(0)).toBe(false);
      expect(MathUtils.isPrime(1)).toBe(false);
      expect(MathUtils.isPrime(-1)).toBe(false);
    });
  });
});

有状态类的测试

javascript
// shopping-cart.js
class ShoppingCart {
  constructor() {
    this.items = [];
    this.discountRate = 0;
  }
  
  addItem(product, quantity = 1) {
    if (!product || !product.id || !product.price) {
      throw new Error('Invalid product');
    }
    
    if (quantity <= 0) {
      throw new Error('Quantity must be positive');
    }
    
    const existingItem = this.items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
  }
  
  removeItem(productId) {
    const index = this.items.findIndex(item => item.product.id === productId);
    if (index !== -1) {
      this.items.splice(index, 1);
    }
  }
  
  updateQuantity(productId, quantity) {
    if (quantity <= 0) {
      throw new Error('Quantity must be positive');
    }
    
    const item = this.items.find(item => item.product.id === productId);
    if (item) {
      item.quantity = quantity;
    }
  }
  
  setDiscount(rate) {
    if (rate < 0 || rate > 1) {
      throw new Error('Discount rate must be between 0 and 1');
    }
    this.discountRate = rate;
  }
  
  getTotal() {
    const subtotal = this.items.reduce(
      (sum, item) => sum + (item.product.price * item.quantity),
      0
    );
    return subtotal * (1 - this.discountRate);
  }
  
  getItemCount() {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }
  
  clear() {
    this.items = [];
    this.discountRate = 0;
  }
  
  isEmpty() {
    return this.items.length === 0;
  }
}

module.exports = ShoppingCart;
javascript
// shopping-cart.test.js
const ShoppingCart = require('./shopping-cart');

describe('ShoppingCart', () => {
  let cart;
  let sampleProducts;
  
  beforeEach(() => {
    cart = new ShoppingCart();
    sampleProducts = [
      { id: '1', name: 'Apple', price: 1.99 },
      { id: '2', name: 'Banana', price: 0.99 },
      { id: '3', name: 'Orange', price: 2.49 }
    ];
  });
  
  describe('初始状态', () => {
    it('应该创建空购物车', () => {
      expect(cart.isEmpty()).toBe(true);
      expect(cart.getItemCount()).toBe(0);
      expect(cart.getTotal()).toBe(0);
    });
  });
  
  describe('添加商品', () => {
    it('应该成功添加单个商品', () => {
      cart.addItem(sampleProducts[0], 2);
      
      expect(cart.isEmpty()).toBe(false);
      expect(cart.getItemCount()).toBe(2);
      expect(cart.items).toHaveLength(1);
      expect(cart.items[0]).toEqual({
        product: sampleProducts[0],
        quantity: 2
      });
    });
    
    it('应该合并相同商品', () => {
      cart.addItem(sampleProducts[0], 2);
      cart.addItem(sampleProducts[0], 3);
      
      expect(cart.items).toHaveLength(1);
      expect(cart.items[0].quantity).toBe(5);
      expect(cart.getItemCount()).toBe(5);
    });
    
    it('应该添加不同商品到购物车', () => {
      cart.addItem(sampleProducts[0], 1);
      cart.addItem(sampleProducts[1], 2);
      
      expect(cart.items).toHaveLength(2);
      expect(cart.getItemCount()).toBe(3);
    });
    
    it('应该在数量为默认值时添加1个商品', () => {
      cart.addItem(sampleProducts[0]);
      
      expect(cart.items[0].quantity).toBe(1);
    });
    
    it('应该拒绝无效商品', () => {
      expect(() => cart.addItem(null)).toThrow('Invalid product');
      expect(() => cart.addItem({})).toThrow('Invalid product');
      expect(() => cart.addItem({ id: '1' })).toThrow('Invalid product');
      expect(() => cart.addItem({ price: 1.99 })).toThrow('Invalid product');
    });
    
    it('应该拒绝无效数量', () => {
      expect(() => cart.addItem(sampleProducts[0], 0)).toThrow('Quantity must be positive');
      expect(() => cart.addItem(sampleProducts[0], -1)).toThrow('Quantity must be positive');
    });
  });
  
  describe('移除商品', () => {
    beforeEach(() => {
      cart.addItem(sampleProducts[0], 2);
      cart.addItem(sampleProducts[1], 1);
    });
    
    it('应该成功移除存在的商品', () => {
      cart.removeItem('1');
      
      expect(cart.items).toHaveLength(1);
      expect(cart.items[0].product.id).toBe('2');
      expect(cart.getItemCount()).toBe(1);
    });
    
    it('应该忽略不存在的商品ID', () => {
      const originalLength = cart.items.length;
      cart.removeItem('999');
      
      expect(cart.items).toHaveLength(originalLength);
    });
  });
  
  describe('更新数量', () => {
    beforeEach(() => {
      cart.addItem(sampleProducts[0], 2);
    });
    
    it('应该成功更新商品数量', () => {
      cart.updateQuantity('1', 5);
      
      expect(cart.items[0].quantity).toBe(5);
      expect(cart.getItemCount()).toBe(5);
    });
    
    it('应该忽略不存在的商品ID', () => {
      cart.updateQuantity('999', 5);
      
      expect(cart.items[0].quantity).toBe(2); // 保持原数量
    });
    
    it('应该拒绝无效数量', () => {
      expect(() => cart.updateQuantity('1', 0)).toThrow('Quantity must be positive');
      expect(() => cart.updateQuantity('1', -1)).toThrow('Quantity must be positive');
    });
  });
  
  describe('折扣功能', () => {
    beforeEach(() => {
      cart.addItem(sampleProducts[0], 1); // $1.99
      cart.addItem(sampleProducts[1], 2); // $0.99 x 2 = $1.98
      // 总计: $3.97
    });
    
    it('应该正确应用折扣', () => {
      cart.setDiscount(0.1); // 10% 折扣
      
      expect(cart.getTotal()).toBeCloseTo(3.97 * 0.9);
    });
    
    it('应该处理0折扣', () => {
      cart.setDiscount(0);
      
      expect(cart.getTotal()).toBeCloseTo(3.97);
    });
    
    it('应该处理100%折扣', () => {
      cart.setDiscount(1);
      
      expect(cart.getTotal()).toBe(0);
    });
    
    it('应该拒绝无效折扣率', () => {
      expect(() => cart.setDiscount(-0.1)).toThrow('Discount rate must be between 0 and 1');
      expect(() => cart.setDiscount(1.1)).toThrow('Discount rate must be between 0 and 1');
    });
  });
  
  describe('计算总价', () => {
    it('应该正确计算多个商品的总价', () => {
      cart.addItem(sampleProducts[0], 2); // $1.99 x 2 = $3.98
      cart.addItem(sampleProducts[1], 3); // $0.99 x 3 = $2.97
      cart.addItem(sampleProducts[2], 1); // $2.49 x 1 = $2.49
      // 总计: $9.44
      
      expect(cart.getTotal()).toBeCloseTo(9.44);
    });
    
    it('应该在应用折扣后正确计算总价', () => {
      cart.addItem(sampleProducts[0], 1); // $1.99
      cart.setDiscount(0.2); // 20% 折扣
      
      expect(cart.getTotal()).toBeCloseTo(1.99 * 0.8);
    });
  });
  
  describe('清空购物车', () => {
    beforeEach(() => {
      cart.addItem(sampleProducts[0], 2);
      cart.addItem(sampleProducts[1], 1);
      cart.setDiscount(0.1);
    });
    
    it('应该清空所有商品和重置状态', () => {
      cart.clear();
      
      expect(cart.isEmpty()).toBe(true);
      expect(cart.getItemCount()).toBe(0);
      expect(cart.getTotal()).toBe(0);
      expect(cart.items).toHaveLength(0);
    });
  });
});

异步函数测试

javascript
// async-user-service.js
const axios = require('axios');

class AsyncUserService {
  constructor(baseURL = 'https://api.example.com') {
    this.baseURL = baseURL;
    this.cache = new Map();
    this.requestQueue = [];
  }
  
  async getUser(id) {
    // 缓存检查
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }
    
    try {
      const response = await axios.get(`${this.baseURL}/users/${id}`);
      const user = response.data;
      
      // 缓存结果
      this.cache.set(id, user);
      
      return user;
    } catch (error) {
      if (error.response?.status === 404) {
        throw new Error(`User ${id} not found`);
      }
      throw new Error('Failed to fetch user');
    }
  }
  
  async createUser(userData) {
    return new Promise((resolve, reject) => {
      const request = {
        id: Date.now(),
        userData,
        resolve,
        reject
      };
      
      this.requestQueue.push(request);
      this.processQueue();
    });
  }
  
  async processQueue() {
    if (this.requestQueue.length === 0) return;
    
    const request = this.requestQueue.shift();
    
    try {
      // 模拟异步处理
      await new Promise(resolve => setTimeout(resolve, 100));
      
      const response = await axios.post(`${this.baseURL}/users`, request.userData);
      request.resolve(response.data);
    } catch (error) {
      request.reject(error);
    }
    
    // 继续处理队列
    if (this.requestQueue.length > 0) {
      setImmediate(() => this.processQueue());
    }
  }
  
  async batchGetUsers(ids) {
    const promises = ids.map(id => this.getUser(id));
    const results = await Promise.allSettled(promises);
    
    return results.map((result, index) => ({
      id: ids[index],
      success: result.status === 'fulfilled',
      data: result.status === 'fulfilled' ? result.value : null,
      error: result.status === 'rejected' ? result.reason.message : null
    }));
  }
  
  clearCache() {
    this.cache.clear();
  }
}

module.exports = AsyncUserService;
javascript
// async-user-service.test.js
const axios = require('axios');
const AsyncUserService = require('./async-user-service');

jest.mock('axios');
const mockedAxios = axios;

describe('AsyncUserService', () => {
  let userService;
  
  beforeEach(() => {
    userService = new AsyncUserService();
    jest.clearAllMocks();
  });
  
  describe('getUser', () => {
    const sampleUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
    
    it('应该成功获取用户数据', async () => {
      mockedAxios.get.mockResolvedValue({ data: sampleUser });
      
      const result = await userService.getUser('1');
      
      expect(result).toEqual(sampleUser);
      expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
    });
    
    it('应该缓存用户数据', async () => {
      mockedAxios.get.mockResolvedValue({ data: sampleUser });
      
      // 第一次调用
      const result1 = await userService.getUser('1');
      // 第二次调用
      const result2 = await userService.getUser('1');
      
      expect(result1).toEqual(sampleUser);
      expect(result2).toEqual(sampleUser);
      expect(mockedAxios.get).toHaveBeenCalledTimes(1); // 只调用一次API
    });
    
    it('应该处理用户不存在的情况', async () => {
      mockedAxios.get.mockRejectedValue({
        response: { status: 404 }
      });
      
      await expect(userService.getUser('999'))
        .rejects
        .toThrow('User 999 not found');
    });
    
    it('应该处理网络错误', async () => {
      mockedAxios.get.mockRejectedValue(new Error('Network error'));
      
      await expect(userService.getUser('1'))
        .rejects
        .toThrow('Failed to fetch user');
    });
  });
  
  describe('createUser', () => {
    const userData = { name: 'Jane Doe', email: 'jane@example.com' };
    const createdUser = { id: '2', ...userData };
    
    it('应该成功创建用户', async () => {
      mockedAxios.post.mockResolvedValue({ data: createdUser });
      
      const result = await userService.createUser(userData);
      
      expect(result).toEqual(createdUser);
      expect(mockedAxios.post).toHaveBeenCalledWith(
        'https://api.example.com/users',
        userData
      );
    });
    
    it('应该按顺序处理多个创建请求', async () => {
      const users = [
        { name: 'User 1', email: 'user1@example.com' },
        { name: 'User 2', email: 'user2@example.com' },
        { name: 'User 3', email: 'user3@example.com' }
      ];
      
      const responses = users.map((user, index) => ({
        data: { id: String(index + 1), ...user }
      }));
      
      mockedAxios.post
        .mockResolvedValueOnce(responses[0])
        .mockResolvedValueOnce(responses[1])
        .mockResolvedValueOnce(responses[2]);
      
      const promises = users.map(user => userService.createUser(user));
      const results = await Promise.all(promises);
      
      expect(results).toHaveLength(3);
      expect(mockedAxios.post).toHaveBeenCalledTimes(3);
    });
  });
  
  describe('batchGetUsers', () => {
    it('应该批量获取用户数据', async () => {
      const users = [
        { id: '1', name: 'User 1' },
        { id: '2', name: 'User 2' },
        { id: '3', name: 'User 3' }
      ];
      
      mockedAxios.get
        .mockResolvedValueOnce({ data: users[0] })
        .mockResolvedValueOnce({ data: users[1] })
        .mockResolvedValueOnce({ data: users[2] });
      
      const results = await userService.batchGetUsers(['1', '2', '3']);
      
      expect(results).toEqual([
        { id: '1', success: true, data: users[0], error: null },
        { id: '2', success: true, data: users[1], error: null },
        { id: '3', success: true, data: users[2], error: null }
      ]);
    });
    
    it('应该处理部分失败的情况', async () => {
      const user1 = { id: '1', name: 'User 1' };
      
      mockedAxios.get
        .mockResolvedValueOnce({ data: user1 })
        .mockRejectedValueOnce({ response: { status: 404 } })
        .mockRejectedValueOnce(new Error('Network error'));
      
      const results = await userService.batchGetUsers(['1', '999', '3']);
      
      expect(results).toEqual([
        { id: '1', success: true, data: user1, error: null },
        { id: '999', success: false, data: null, error: 'User 999 not found' },
        { id: '3', success: false, data: null, error: 'Failed to fetch user' }
      ]);
    });
  });
  
  describe('缓存管理', () => {
    it('应该能够清空缓存', async () => {
      const user = { id: '1', name: 'John Doe' };
      mockedAxios.get.mockResolvedValue({ data: user });
      
      // 第一次调用,数据被缓存
      await userService.getUser('1');
      expect(mockedAxios.get).toHaveBeenCalledTimes(1);
      
      // 清空缓存
      userService.clearCache();
      
      // 再次调用,应该重新请求API
      await userService.getUser('1');
      expect(mockedAxios.get).toHaveBeenCalledTimes(2);
    });
  });
});

🎯 边界条件和异常测试

边界值分析

javascript
// validation-service.js
class ValidationService {
  static validateAge(age) {
    if (typeof age !== 'number') {
      throw new Error('Age must be a number');
    }
    
    if (age < 0) {
      throw new Error('Age cannot be negative');
    }
    
    if (age > 150) {
      throw new Error('Age cannot exceed 150');
    }
    
    return age >= 18;
  }
  
  static validateEmail(email) {
    if (typeof email !== 'string') {
      throw new Error('Email must be a string');
    }
    
    if (email.length === 0) {
      throw new Error('Email cannot be empty');
    }
    
    if (email.length > 254) {
      throw new Error('Email too long');
    }
    
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  static validatePassword(password) {
    if (typeof password !== 'string') {
      throw new Error('Password must be a string');
    }
    
    const errors = [];
    
    if (password.length < 8) {
      errors.push('Password must be at least 8 characters long');
    }
    
    if (password.length > 128) {
      errors.push('Password cannot exceed 128 characters');
    }
    
    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain at least one uppercase letter');
    }
    
    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain at least one lowercase letter');
    }
    
    if (!/\d/.test(password)) {
      errors.push('Password must contain at least one number');
    }
    
    if (!/[!@#$%^&*]/.test(password)) {
      errors.push('Password must contain at least one special character');
    }
    
    return {
      isValid: errors.length === 0,
      errors
    };
  }
}

module.exports = ValidationService;
javascript
// validation-service.test.js
const ValidationService = require('./validation-service');

describe('ValidationService', () => {
  describe('validateAge', () => {
    // 正常范围测试
    describe('有效年龄', () => {
      it('应该验证成年人年龄', () => {
        expect(ValidationService.validateAge(18)).toBe(true);
        expect(ValidationService.validateAge(25)).toBe(true);
        expect(ValidationService.validateAge(65)).toBe(true);
      });
      
      it('应该验证未成年人年龄', () => {
        expect(ValidationService.validateAge(17)).toBe(false);
        expect(ValidationService.validateAge(10)).toBe(false);
        expect(ValidationService.validateAge(0)).toBe(false);
      });
    });
    
    // 边界值测试
    describe('边界值', () => {
      it('应该正确处理边界年龄', () => {
        expect(ValidationService.validateAge(0)).toBe(false);    // 最小值
        expect(ValidationService.validateAge(17)).toBe(false);   // 成年边界-1
        expect(ValidationService.validateAge(18)).toBe(true);    // 成年边界
        expect(ValidationService.validateAge(150)).toBe(true);   // 最大值
      });
    });
    
    // 异常值测试
    describe('异常值', () => {
      it('应该拒绝负数年龄', () => {
        expect(() => ValidationService.validateAge(-1)).toThrow('Age cannot be negative');
        expect(() => ValidationService.validateAge(-100)).toThrow('Age cannot be negative');
      });
      
      it('应该拒绝过大年龄', () => {
        expect(() => ValidationService.validateAge(151)).toThrow('Age cannot exceed 150');
        expect(() => ValidationService.validateAge(1000)).toThrow('Age cannot exceed 150');
      });
      
      it('应该拒绝非数字类型', () => {
        expect(() => ValidationService.validateAge('18')).toThrow('Age must be a number');
        expect(() => ValidationService.validateAge(null)).toThrow('Age must be a number');
        expect(() => ValidationService.validateAge(undefined)).toThrow('Age must be a number');
        expect(() => ValidationService.validateAge({})).toThrow('Age must be a number');
      });
    });
  });
  
  describe('validateEmail', () => {
    describe('有效邮箱', () => {
      it('应该验证标准邮箱格式', () => {
        expect(ValidationService.validateEmail('test@example.com')).toBe(true);
        expect(ValidationService.validateEmail('user.name@domain.co.uk')).toBe(true);
        expect(ValidationService.validateEmail('user+tag@example.org')).toBe(true);
      });
    });
    
    describe('无效邮箱', () => {
      it('应该拒绝错误格式的邮箱', () => {
        expect(ValidationService.validateEmail('invalid-email')).toBe(false);
        expect(ValidationService.validateEmail('@example.com')).toBe(false);
        expect(ValidationService.validateEmail('test@')).toBe(false);
        expect(ValidationService.validateEmail('test.example.com')).toBe(false);
      });
    });
    
    describe('边界值和异常', () => {
      it('应该处理空字符串', () => {
        expect(() => ValidationService.validateEmail('')).toThrow('Email cannot be empty');
      });
      
      it('应该处理过长邮箱', () => {
        const longEmail = 'a'.repeat(250) + '@example.com';
        expect(() => ValidationService.validateEmail(longEmail)).toThrow('Email too long');
      });
      
      it('应该拒绝非字符串类型', () => {
        expect(() => ValidationService.validateEmail(123)).toThrow('Email must be a string');
        expect(() => ValidationService.validateEmail(null)).toThrow('Email must be a string');
      });
    });
  });
  
  describe('validatePassword', () => {
    describe('有效密码', () => {
      it('应该验证符合所有要求的密码', () => {
        const result = ValidationService.validatePassword('SecurePass123!');
        expect(result.isValid).toBe(true);
        expect(result.errors).toHaveLength(0);
      });
    });
    
    describe('无效密码', () => {
      it('应该检测过短密码', () => {
        const result = ValidationService.validatePassword('Short1!');
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password must be at least 8 characters long');
      });
      
      it('应该检测缺少大写字母', () => {
        const result = ValidationService.validatePassword('lowercase123!');
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password must contain at least one uppercase letter');
      });
      
      it('应该检测缺少小写字母', () => {
        const result = ValidationService.validatePassword('UPPERCASE123!');
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password must contain at least one lowercase letter');
      });
      
      it('应该检测缺少数字', () => {
        const result = ValidationService.validatePassword('NoNumbers!');
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password must contain at least one number');
      });
      
      it('应该检测缺少特殊字符', () => {
        const result = ValidationService.validatePassword('NoSpecialChar123');
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password must contain at least one special character');
      });
      
      it('应该返回多个错误', () => {
        const result = ValidationService.validatePassword('bad');
        expect(result.isValid).toBe(false);
        expect(result.errors.length).toBeGreaterThan(1);
        expect(result.errors).toContain('Password must be at least 8 characters long');
        expect(result.errors).toContain('Password must contain at least one uppercase letter');
      });
    });
    
    describe('边界值', () => {
      it('应该处理最小长度密码', () => {
        const result = ValidationService.validatePassword('MinLen1!');
        expect(result.isValid).toBe(true);
      });
      
      it('应该处理最大长度密码', () => {
        const longPassword = 'A1!' + 'a'.repeat(125);
        const result = ValidationService.validatePassword(longPassword);
        expect(result.isValid).toBe(true);
      });
      
      it('应该拒绝超长密码', () => {
        const tooLongPassword = 'A1!' + 'a'.repeat(126);
        const result = ValidationService.validatePassword(tooLongPassword);
        expect(result.isValid).toBe(false);
        expect(result.errors).toContain('Password cannot exceed 128 characters');
      });
    });
  });
});

📊 测试数据管理

测试数据工厂

javascript
// test-data-factory.js
const faker = require('faker');

class TestDataFactory {
  static seed(seed = 12345) {
    faker.seed(seed);
  }
  
  static createUser(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      name: faker.name.findName(),
      email: faker.internet.email().toLowerCase(),
      age: faker.datatype.number({ min: 18, max: 80 }),
      phone: faker.phone.phoneNumber(),
      address: {
        street: faker.address.streetAddress(),
        city: faker.address.city(),
        state: faker.address.state(),
        zipCode: faker.address.zipCode(),
        country: faker.address.country()
      },
      createdAt: faker.date.past(),
      isActive: faker.datatype.boolean(),
      ...overrides
    };
  }
  
  static createProduct(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      name: faker.commerce.productName(),
      description: faker.commerce.productDescription(),
      price: parseFloat(faker.commerce.price()),
      category: faker.commerce.department(),
      inStock: faker.datatype.number({ min: 0, max: 100 }),
      rating: faker.datatype.float({ min: 1, max: 5, precision: 0.1 }),
      createdAt: faker.date.past(),
      ...overrides
    };
  }
  
  static createOrder(overrides = {}) {
    const items = Array.from(
      { length: faker.datatype.number({ min: 1, max: 5 }) },
      () => ({
        product: this.createProduct(),
        quantity: faker.datatype.number({ min: 1, max: 3 }),
        price: parseFloat(faker.commerce.price())
      })
    );
    
    return {
      id: faker.datatype.uuid(),
      userId: faker.datatype.uuid(),
      items,
      total: items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
      status: faker.random.arrayElement(['pending', 'processing', 'shipped', 'delivered']),
      createdAt: faker.date.past(),
      ...overrides
    };
  }
  
  // 预定义的测试场景数据
  static scenarios = {
    validUser: () => ({
      name: 'John Doe',
      email: 'john@example.com',
      age: 30,
      phone: '+1234567890'
    }),
    
    minorUser: () => ({
      name: 'Jane Minor',
      email: 'jane@example.com',
      age: 16
    }),
    
    seniorUser: () => ({
      name: 'Bob Senior',
      email: 'bob@example.com',
      age: 75
    }),
    
    invalidEmailUser: () => ({
      name: 'Invalid User',
      email: 'invalid-email',
      age: 25
    }),
    
    expensiveProduct: () => ({
      name: 'Luxury Item',
      price: 999.99,
      category: 'Luxury'
    }),
    
    freeProduct: () => ({
      name: 'Free Sample',
      price: 0,
      category: 'Sample'
    }),
    
    outOfStockProduct: () => ({
      name: 'Out of Stock Item',
      price: 29.99,
      inStock: 0
    })
  };
  
  static createBatch(factory, count, overrides = {}) {
    return Array.from({ length: count }, () => factory(overrides));
  }
  
  static createUserBatch(count, overrides = {}) {
    return this.createBatch(this.createUser.bind(this), count, overrides);
  }
  
  static createProductBatch(count, overrides = {}) {
    return this.createBatch(this.createProduct.bind(this), count, overrides);
  }
}

module.exports = TestDataFactory;

测试数据使用示例

javascript
// using-test-data.test.js
const TestDataFactory = require('./test-data-factory');
const UserService = require('./user-service');

describe('UserService with Test Data', () => {
  let userService;
  
  beforeEach(() => {
    userService = new UserService();
    // 设置确定性的随机种子,确保测试可重复
    TestDataFactory.seed();
  });
  
  describe('使用预定义场景', () => {
    it('应该处理有效用户', () => {
      const validUser = TestDataFactory.scenarios.validUser();
      
      const result = userService.validateUser(validUser);
      
      expect(result.isValid).toBe(true);
      expect(result.user.name).toBe('John Doe');
    });
    
    it('应该拒绝未成年用户', () => {
      const minorUser = TestDataFactory.scenarios.minorUser();
      
      const result = userService.validateUser(minorUser);
      
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('User must be 18 or older');
    });
    
    it('应该拒绝无效邮箱', () => {
      const invalidUser = TestDataFactory.scenarios.invalidEmailUser();
      
      const result = userService.validateUser(invalidUser);
      
      expect(result.isValid).toBe(false);
      expect(result.errors).toContain('Invalid email format');
    });
  });
  
  describe('使用动态生成数据', () => {
    it('应该处理随机生成的用户', () => {
      const randomUser = TestDataFactory.createUser({
        age: 25,
        email: 'test@example.com'
      });
      
      const result = userService.validateUser(randomUser);
      
      expect(result.isValid).toBe(true);
      expect(randomUser.age).toBe(25);
      expect(randomUser.email).toBe('test@example.com');
    });
    
    it('应该处理批量用户数据', () => {
      const users = TestDataFactory.createUserBatch(10, { isActive: true });
      
      const results = users.map(user => userService.validateUser(user));
      const validUsers = results.filter(r => r.isValid);
      
      expect(users).toHaveLength(10);
      expect(validUsers.length).toBeGreaterThan(0);
      users.forEach(user => {
        expect(user.isActive).toBe(true);
      });
    });
  });
  
  describe('边界值数据测试', () => {
    it('应该测试年龄边界值', () => {
      const boundaryAges = [17, 18, 150, 151];
      
      boundaryAges.forEach(age => {
        const user = TestDataFactory.createUser({ age });
        const result = userService.validateUser(user);
        
        if (age < 18 || age > 150) {
          expect(result.isValid).toBe(false);
        } else {
          expect(result.isValid).toBe(true);
        }
      });
    });
  });
  
  describe('参数化测试', () => {
    const testCases = [
      { email: 'valid@example.com', expected: true },
      { email: 'invalid-email', expected: false },
      { email: '', expected: false },
      { email: 'user@domain', expected: false },
      { email: '@domain.com', expected: false }
    ];
    
    test.each(testCases)('email $email should be $expected', ({ email, expected }) => {
      const user = TestDataFactory.createUser({ email });
      const result = userService.validateUser(user);
      
      expect(result.isValid).toBe(expected);
    });
  });
});

📝 测试用例最佳实践

测试命名约定

javascript
// 好的测试命名
describe('UserService', () => {
  describe('createUser', () => {
    it('应该在提供有效数据时创建新用户', () => {});
    it('应该在邮箱已存在时抛出错误', () => {});
    it('应该在年龄小于18时拒绝创建', () => {});
  });
});

// 避免的测试命名
describe('UserService', () => {
  it('测试创建用户', () => {}); // 太模糊
  it('test1', () => {}); // 没有意义
  it('should work', () => {}); // 不具体
});

测试组织结构

javascript
// 良好的测试组织
describe('购物车系统', () => {
  describe('添加商品功能', () => {
    describe('有效商品', () => {
      it('应该添加单个商品到空购物车', () => {});
      it('应该合并相同商品的数量', () => {});
      it('应该添加不同商品到购物车', () => {});
    });
    
    describe('无效商品', () => {
      it('应该拒绝null商品', () => {});
      it('应该拒绝没有ID的商品', () => {});
      it('应该拒绝负价格的商品', () => {});
    });
  });
  
  describe('移除商品功能', () => {
    beforeEach(() => {
      // 为移除测试设置初始状态
    });
    
    it('应该移除存在的商品', () => {});
    it('应该忽略不存在的商品', () => {});
  });
});

📝 总结

高质量的测试用例编写需要:

  • 清晰的结构:使用AAA模式组织测试
  • 全面的覆盖:正常流程、边界值、异常情况
  • 良好的命名:测试意图明确易懂
  • 数据管理:使用工厂模式管理测试数据
  • 可维护性:遵循FIRST原则,保持测试独立

通过系统化的测试用例编写,可以确保代码质量和功能正确性。

🔗 相关资源