您现在的位置是:网站首页 > 模拟(Mock)与桩(Stub)在模式测试中的应用文章详情
模拟(Mock)与桩(Stub)在模式测试中的应用
陈川
【
JavaScript
】
38568人已围观
8115字
模拟(Mock)与桩(Stub)的基本概念
模拟和桩是测试替身(Test Double)的两种常见形式,用于隔离被测代码与外部依赖。模拟对象用于验证被测代码与依赖之间的交互行为,而桩对象则用于提供预定义的响应数据。在JavaScript测试中,这两种技术经常结合使用,特别是在单元测试场景下。
模拟对象通常会记录调用信息,并在测试结束时验证这些调用是否符合预期。例如,验证某个方法是否被调用、调用次数以及参数是否正确:
// 使用Jest创建模拟函数
const mockFn = jest.fn()
.mockReturnValueOnce(42)
.mockReturnValueOnce('default');
test('mock function behavior', () => {
expect(mockFn()).toBe(42);
expect(mockFn()).toBe('default');
expect(mockFn).toHaveBeenCalledTimes(2);
});
桩对象则更简单,它只是为被测代码提供必要的输入数据,不关心这些数据如何被使用:
// 创建一个简单的桩对象
const userStub = {
id: 1,
name: 'Test User',
email: 'test@example.com'
};
function getUserName(user) {
return user.name;
}
test('should return user name', () => {
expect(getUserName(userStub)).toBe('Test User');
});
在单元测试中的应用场景
单元测试的核心目标是隔离测试目标,使其不受外部依赖的影响。模拟和桩在这方面发挥着关键作用。当测试一个依赖于数据库操作的函数时,我们不需要实际连接数据库,而是使用桩来模拟数据库响应:
// 被测函数
async function getUserById(userId, dbClient) {
return dbClient.query('SELECT * FROM users WHERE id = ?', [userId]);
}
// 测试用例
test('should return user by id', async () => {
// 创建数据库客户端的桩
const dbStub = {
query: jest.fn().mockResolvedValue([{ id: 1, name: 'Mock User' }])
};
const user = await getUserById(1, dbStub);
expect(user).toEqual([{ id: 1, name: 'Mock User' }]);
expect(dbStub.query).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = ?',
[1]
);
});
对于更复杂的交互测试,比如验证某个回调函数是否被正确调用,模拟就变得非常有用:
// 被测函数
function processData(data, callback) {
if (data.valid) {
callback(null, 'Processed successfully');
} else {
callback(new Error('Invalid data'));
}
}
// 测试用例
test('should call callback with success', () => {
const mockCallback = jest.fn();
processData({ valid: true }, mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
null,
'Processed successfully'
);
});
test('should call callback with error', () => {
const mockCallback = jest.fn();
processData({ valid: false }, mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
expect.any(Error),
undefined
);
});
与测试框架的集成实践
现代JavaScript测试框架如Jest、Mocha等都对模拟和桩提供了良好的支持。Jest内置了强大的模拟功能,可以轻松创建函数模拟、模块模拟和定时器模拟:
// 模拟整个模块
jest.mock('../api', () => ({
fetchUser: jest.fn().mockResolvedValue({
id: 1,
name: 'Mocked User'
})
}));
// 测试使用该模块的函数
const { getUser } = require('../userService');
test('should return mocked user', async () => {
const user = await getUser(1);
expect(user).toEqual({
id: 1,
name: 'Mocked User'
});
});
对于更细粒度的控制,可以创建手动模拟(manual mocks)。在Jest中,通过在__mocks__
目录下创建同名文件来实现:
├── src
│ ├── __mocks__
│ │ └── api.js
│ └── api.js
在__mocks__/api.js
中:
export const fetchUser = jest.fn(() =>
Promise.resolve({ id: 1, name: 'Manual Mock User' })
);
然后在测试文件中:
jest.mock('../api');
import { fetchUser } from '../api';
import { getUser } from '../userService';
test('should use manual mock', async () => {
const user = await getUser(1);
expect(fetchUser).toHaveBeenCalledWith(1);
expect(user).toEqual({
id: 1,
name: 'Manual Mock User'
});
});
异步代码测试中的特殊考虑
测试异步代码时,模拟和桩需要处理Promise和async/await。常见的模式包括模拟异步函数的延迟响应:
// 模拟带有延迟的API调用
const mockApi = {
fetchData: jest.fn()
.mockImplementationOnce(() =>
new Promise(resolve =>
setTimeout(() => resolve('data'), 100)
)
)
};
test('should handle async response', async () => {
const promise = mockApi.fetchData();
expect(mockApi.fetchData).toHaveBeenCalled();
const result = await promise;
expect(result).toBe('data');
});
对于更复杂的异步流程,可以使用mockResolvedValue和mockRejectedValue:
const paymentService = {
processPayment: jest.fn()
.mockResolvedValueOnce({ success: true })
.mockRejectedValueOnce(new Error('Insufficient funds'))
};
test('should handle successful payment', async () => {
await expect(paymentService.processPayment({ amount: 100 }))
.resolves.toEqual({ success: true });
});
test('should handle payment failure', async () => {
await expect(paymentService.processPayment({ amount: 1000 }))
.rejects.toThrow('Insufficient funds');
});
测试驱动开发(TDD)中的应用
在测试驱动开发中,模拟和桩可以帮助我们在实现实际功能前定义接口行为。例如,开发一个购物车功能时,可以先定义其依赖的库存服务接口:
// 测试文件
describe('ShoppingCart', () => {
let cart;
const mockInventory = {
checkStock: jest.fn(),
reduceStock: jest.fn()
};
beforeEach(() => {
cart = new ShoppingCart(mockInventory);
mockInventory.checkStock.mockReset();
mockInventory.reduceStock.mockReset();
});
test('should add item when in stock', () => {
mockInventory.checkStock.mockReturnValue(true);
cart.addItem('product1', 1);
expect(cart.items).toHaveLength(1);
expect(mockInventory.checkStock)
.toHaveBeenCalledWith('product1', 1);
});
test('should not add item when out of stock', () => {
mockInventory.checkStock.mockReturnValue(false);
expect(() => cart.addItem('product1', 1))
.toThrow('Out of stock');
});
});
然后逐步实现ShoppingCart类:
class ShoppingCart {
constructor(inventory) {
this.inventory = inventory;
this.items = [];
}
addItem(productId, quantity) {
const inStock = this.inventory.checkStock(productId, quantity);
if (!inStock) {
throw new Error('Out of stock');
}
this.items.push({ productId, quantity });
}
}
边界情况和错误处理
良好的测试应该覆盖各种边界情况和错误处理路径。模拟和桩可以帮助我们轻松模拟这些不常见的情况:
// 测试网络请求失败
const mockHttp = {
get: jest.fn()
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({ data: 'retry success' })
};
async function fetchWithRetry(url, retries = 3) {
try {
return await mockHttp.get(url);
} catch (err) {
if (retries <= 0) throw err;
return fetchWithRetry(url, retries - 1);
}
}
test('should retry on network failure', async () => {
const result = await fetchWithRetry('api/data');
expect(result).toEqual({ data: 'retry success' });
expect(mockHttp.get).toHaveBeenCalledTimes(2);
});
test('should throw after max retries', async () => {
mockHttp.get.mockRejectedValue(new Error('Network error'));
await expect(fetchWithRetry('api/data', 1))
.rejects.toThrow('Network error');
expect(mockHttp.get).toHaveBeenCalledTimes(2);
});
性能测试中的应用
虽然模拟和桩主要用于单元测试,但它们也可以在某些性能测试场景中发挥作用。例如,模拟慢速的第三方服务来测试应用的超时处理:
function createSlowService(delay) {
return {
fetch: jest.fn(() =>
new Promise(resolve =>
setTimeout(() => resolve('data'), delay)
)
)
};
}
test('should timeout slow service', async () => {
const slowService = createSlowService(1000);
const promise = Promise.race([
slowService.fetch(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 500)
)
]);
await expect(promise).rejects.toThrow('Timeout');
});
与真实测试的平衡
虽然模拟和桩非常有用,但过度使用可能导致测试与实现耦合过紧。一个好的经验法则是:
- 对IO密集型操作(网络、数据库)使用模拟/桩
- 对纯函数或简单对象使用真实实现
- 定期进行集成测试以验证模拟行为与真实环境一致
// 不推荐的过度模拟
class OrderProcessor {
constructor(paymentService, inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
process(order) {
// 复杂逻辑...
}
}
// 测试中过度模拟所有依赖
test('over-mocked test', () => {
const mockPayment = { charge: jest.fn() };
const mockInventory = { reduce: jest.fn() };
const processor = new OrderProcessor(mockPayment, mockInventory);
processor.process({ items: [] });
expect(mockPayment.charge).toHaveBeenCalled();
expect(mockInventory.reduce).toHaveBeenCalled();
});
// 更好的方式:测试核心逻辑,只模拟必要的外部依赖
test('better focused test', () => {
const mockPayment = { charge: jest.fn() };
// 使用真实的内存库存服务
const inventory = new MemoryInventory();
const processor = new OrderProcessor(mockPayment, inventory);
// 测试特定场景
inventory.add('item1', 10);
processor.process({ items: [{ id: 'item1', qty: 1 }] });
expect(mockPayment.charge).toHaveBeenCalled();
expect(inventory.getStock('item1')).toBe(9);
});
上一篇: 单元测试中的模式隔离技巧
下一篇: 测试驱动开发(TDD)与设计模式