Jasmine Spy 功能 #
什么是 Spy? #
Spy(间谍)是测试替身的一种,用于监视函数的调用、记录调用信息、控制返回值等。Spy 可以帮助我们在测试中隔离依赖,专注于测试目标代码。
text
┌─────────────────────────────────────────────────────────────┐
│ Spy 功能概览 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 调用跟踪 │ │ 返回值控制 │ │ 调用验证 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ spyOn │ │ createSpy │ │ createSpyObj │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
spyOn - 监视对象方法 #
基本用法 #
使用 spyOn 监视对象的现有方法:
javascript
describe('spyOn basics', function() {
const calculator = {
add: function(a, b) {
return a + b;
},
multiply: function(a, b) {
return a * b;
}
};
it('should track calls', function() {
spyOn(calculator, 'add');
calculator.add(1, 2);
expect(calculator.add).toHaveBeenCalled();
expect(calculator.add).toHaveBeenCalledWith(1, 2);
});
it('should track multiple calls', function() {
spyOn(calculator, 'add');
calculator.add(1, 2);
calculator.add(3, 4);
calculator.add(5, 6);
expect(calculator.add).toHaveBeenCalledTimes(3);
expect(calculator.add).toHaveBeenCalledWith(1, 2);
expect(calculator.add).toHaveBeenCalledWith(3, 4);
});
});
控制返回值 #
javascript
describe('controlling return values', function() {
const api = {
fetchUser: function(id) {
return { id: id, name: 'Real User' };
}
};
it('should return stubbed value', function() {
spyOn(api, 'fetchUser').and.returnValue({ id: 1, name: 'Mock User' });
const result = api.fetchUser(1);
expect(result.name).toBe('Mock User');
});
it('should return different values for each call', function() {
spyOn(api, 'fetchUser').and.returnValues(
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' }
);
expect(api.fetchUser(1).name).toBe('User 1');
expect(api.fetchUser(2).name).toBe('User 2');
expect(api.fetchUser(3).name).toBe('User 3');
});
it('should call through to original', function() {
spyOn(api, 'fetchUser').and.callThrough();
const result = api.fetchUser(1);
expect(result.name).toBe('Real User');
expect(api.fetchUser).toHaveBeenCalled();
});
it('should call fake function', function() {
spyOn(api, 'fetchUser').and.callFake(function(id) {
return { id: id, name: 'Fake User', fake: true };
});
const result = api.fetchUser(1);
expect(result.fake).toBe(true);
});
});
抛出错误 #
javascript
describe('throwing errors', function() {
const service = {
validate: function(data) {
if (!data) throw new Error('Invalid data');
return true;
}
};
it('should throw error', function() {
spyOn(service, 'validate').and.throwError('Validation failed');
expect(function() {
service.validate({ valid: true });
}).toThrowError('Validation failed');
});
});
createSpy - 创建独立 Spy #
基本用法 #
创建一个独立的 Spy 函数:
javascript
describe('createSpy', function() {
it('should create standalone spy', function() {
const callback = jasmine.createSpy('callback');
callback('argument');
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith('argument');
});
it('should use as callback', function() {
const callback = jasmine.createSpy('callback');
function processData(data, cb) {
cb(data);
}
processData('test', callback);
expect(callback).toHaveBeenCalledWith('test');
});
});
带返回值的 Spy #
javascript
describe('createSpy with return value', function() {
it('should return specified value', function() {
const getValue = jasmine.createSpy('getValue').and.returnValue(42);
expect(getValue()).toBe(42);
});
it('should use fake implementation', function() {
const calculate = jasmine.createSpy('calculate').and.callFake(function(a, b) {
return a * b;
});
expect(calculate(3, 4)).toBe(12);
});
});
createSpyObj - 创建 Spy 对象 #
基本用法 #
创建一个包含多个 Spy 方法的对象:
javascript
describe('createSpyObj', function() {
it('should create spy object with methods', function() {
const mockApi = jasmine.createSpyObj('Api', ['get', 'post', 'put', 'delete']);
mockApi.get('/users');
mockApi.post('/users', { name: 'John' });
expect(mockApi.get).toHaveBeenCalledWith('/users');
expect(mockApi.post).toHaveBeenCalledWith('/users', { name: 'John' });
});
it('should create spy object with properties', function() {
const mockConfig = jasmine.createSpyObj('Config', [], {
apiUrl: 'https://api.example.com',
timeout: 5000
});
expect(mockConfig.apiUrl).toBe('https://api.example.com');
expect(mockConfig.timeout).toBe(5000);
});
it('should create spy object with methods and properties', function() {
const mockUser = jasmine.createSpyObj('User', ['save', 'delete'], {
id: 1,
name: 'John'
});
expect(mockUser.id).toBe(1);
expect(mockUser.name).toBe('John');
mockUser.save();
expect(mockUser.save).toHaveBeenCalled();
});
});
设置返回值 #
javascript
describe('createSpyObj with return values', function() {
it('should set return values', function() {
const mockApi = jasmine.createSpyObj('Api', ['get', 'post']);
mockApi.get.and.returnValue({ data: 'mock data' });
mockApi.post.and.returnValue(Promise.resolve({ success: true }));
expect(mockApi.get().data).toBe('mock data');
});
});
Spy 匹配器 #
调用验证 #
javascript
describe('spy matchers', function() {
let spy;
beforeEach(function() {
spy = jasmine.createSpy('spy');
});
it('should check if called', function() {
spy();
expect(spy).toHaveBeenCalled();
});
it('should check if not called', function() {
expect(spy).not.toHaveBeenCalled();
});
it('should check call count', function() {
spy();
spy();
spy();
expect(spy).toHaveBeenCalledTimes(3);
});
it('should check arguments', function() {
spy('arg1', 'arg2');
expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
});
it('should check arguments with multiple calls', function() {
spy('first');
spy('second');
spy('third');
expect(spy).toHaveBeenCalledWith('first');
expect(spy).toHaveBeenCalledWith('second');
expect(spy).toHaveBeenCalledWith('third');
});
});
参数匹配 #
javascript
describe('argument matching', function() {
let spy;
beforeEach(function() {
spy = jasmine.createSpy('spy');
});
it('should match partial arguments', function() {
spy({ id: 1, name: 'John', email: 'john@example.com' });
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ id: 1 }));
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ name: 'John' }));
});
it('should match array containing', function() {
spy([1, 2, 3, 4, 5]);
expect(spy).toHaveBeenCalledWith(jasmine.arrayContaining([1, 3]));
expect(spy).toHaveBeenCalledWith(jasmine.arrayContaining([5]));
});
it('should match any type', function() {
spy('string', 123, true);
expect(spy).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Number), jasmine.any(Boolean));
});
it('should match with custom matcher', function() {
spy({ age: 25 });
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({
age: jasmine.any(Number)
}));
});
});
调用顺序验证 #
javascript
describe('call order', function() {
it('should check call order', function() {
const obj = {
first: function() {},
second: function() {},
third: function() {}
};
spyOn(obj, 'first');
spyOn(obj, 'second');
spyOn(obj, 'third');
obj.first();
obj.second();
obj.third();
expect(obj.first).toHaveBeenCalledBefore(obj.second);
expect(obj.second).toHaveBeenCalledBefore(obj.third);
});
});
Spy 属性 #
访问调用信息 #
javascript
describe('spy properties', function() {
let spy;
beforeEach(function() {
spy = jasmine.createSpy('spy');
});
it('should access calls', function() {
spy('first', 'call');
spy('second', 'call');
expect(spy.calls.count()).toBe(2);
expect(spy.calls.first().args).toEqual(['first', 'call']);
expect(spy.calls.mostRecent().args).toEqual(['second', 'call']);
});
it('should access all calls', function() {
spy(1);
spy(2);
spy(3);
const allCalls = spy.calls.all();
expect(allCalls[0].args).toEqual([1]);
expect(allCalls[1].args).toEqual([2]);
expect(allCalls[2].args).toEqual([3]);
});
it('should access call args', function() {
spy('arg1', 'arg2');
expect(spy.calls.argsFor(0)).toEqual(['arg1', 'arg2']);
});
it('should check if called with specific args', function() {
spy('a', 'b');
spy('c', 'd');
expect(spy.calls.allArgs()).toEqual([['a', 'b'], ['c', 'd']]);
});
it('should reset calls', function() {
spy('first');
spy.calls.reset();
expect(spy).not.toHaveBeenCalled();
expect(spy.calls.count()).toBe(0);
});
});
上下文验证 #
javascript
describe('call context', function() {
it('should track call context', function() {
const obj = {
method: jasmine.createSpy('method')
};
obj.method();
expect(obj.method.calls.first().object).toBe(obj);
});
it('should track context with call', function() {
const obj = {
method: jasmine.createSpy('method')
};
const otherContext = { name: 'other' };
obj.method.call(otherContext);
expect(obj.method.calls.first().object).toBe(otherContext);
});
});
spyOnProperty #
监视属性访问 #
javascript
describe('spyOnProperty', function() {
const user = {
_name: 'John',
get name() {
return this._name;
},
set name(value) {
this._name = value;
}
};
it('should spy on getter', function() {
spyOnProperty(user, 'name', 'get').and.returnValue('Mock Name');
expect(user.name).toBe('Mock Name');
});
it('should spy on setter', function() {
const setterSpy = spyOnProperty(user, 'name', 'set');
user.name = 'Jane';
expect(setterSpy).toHaveBeenCalledWith('Jane');
});
it('should call through getter', function() {
spyOnProperty(user, 'name', 'get').and.callThrough();
expect(user.name).toBe('John');
});
});
实际应用示例 #
模拟 API 服务 #
javascript
describe('UserService', function() {
let userService;
let mockApi;
beforeEach(function() {
mockApi = jasmine.createSpyObj('Api', ['get', 'post', 'put', 'delete']);
userService = new UserService(mockApi);
});
describe('getUser', function() {
it('should fetch user by id', async function() {
mockApi.get.and.returnValue(Promise.resolve({ id: 1, name: 'John' }));
const user = await userService.getUser(1);
expect(mockApi.get).toHaveBeenCalledWith('/users/1');
expect(user.name).toBe('John');
});
it('should handle error', async function() {
mockApi.get.and.returnValue(Promise.reject(new Error('Not found')));
try {
await userService.getUser(999);
fail('should have thrown');
} catch (error) {
expect(error.message).toBe('Not found');
}
});
});
describe('createUser', function() {
it('should create user', async function() {
const userData = { name: 'John', email: 'john@example.com' };
mockApi.post.and.returnValue(Promise.resolve({ id: 1, ...userData }));
const user = await userService.createUser(userData);
expect(mockApi.post).toHaveBeenCalledWith('/users', userData);
expect(user.id).toBe(1);
});
});
});
模拟依赖服务 #
javascript
describe('OrderProcessor', function() {
let orderProcessor;
let mockPaymentService;
let mockInventoryService;
let mockEmailService;
beforeEach(function() {
mockPaymentService = jasmine.createSpyObj('PaymentService', ['charge', 'refund']);
mockInventoryService = jasmine.createSpyObj('InventoryService', ['checkStock', 'reserve']);
mockEmailService = jasmine.createSpyObj('EmailService', ['sendConfirmation']);
orderProcessor = new OrderProcessor(
mockPaymentService,
mockInventoryService,
mockEmailService
);
});
describe('processOrder', function() {
it('should process order successfully', async function() {
const order = { id: 'order-1', items: [], total: 100 };
mockInventoryService.checkStock.and.returnValue(Promise.resolve(true));
mockInventoryService.reserve.and.returnValue(Promise.resolve());
mockPaymentService.charge.and.returnValue(Promise.resolve({ success: true }));
mockEmailService.sendConfirmation.and.returnValue(Promise.resolve());
await orderProcessor.processOrder(order);
expect(mockInventoryService.checkStock).toHaveBeenCalledWith(order.items);
expect(mockInventoryService.reserve).toHaveBeenCalledWith(order.items);
expect(mockPaymentService.charge).toHaveBeenCalledWith(order.total);
expect(mockEmailService.sendConfirmation).toHaveBeenCalled();
});
it('should handle insufficient stock', async function() {
const order = { id: 'order-1', items: [], total: 100 };
mockInventoryService.checkStock.and.returnValue(Promise.resolve(false));
try {
await orderProcessor.processOrder(order);
fail('should have thrown');
} catch (error) {
expect(error.message).toContain('Insufficient stock');
expect(mockPaymentService.charge).not.toHaveBeenCalled();
}
});
});
});
测试回调函数 #
javascript
describe('async callback testing', function() {
describe('DataLoader', function() {
let dataLoader;
let mockDataSource;
beforeEach(function() {
mockDataSource = {
load: jasmine.createSpy('load')
};
dataLoader = new DataLoader(mockDataSource);
});
it('should call callback with data', function(done) {
const callback = jasmine.createSpy('callback').and.callFake(function(data) {
expect(data).toEqual({ items: [1, 2, 3] });
done();
});
mockDataSource.load.and.callFake(function(cb) {
cb({ items: [1, 2, 3] });
});
dataLoader.loadData(callback);
});
});
});
最佳实践 #
1. 在 beforeEach 中设置 Spy #
javascript
describe('with beforeEach', function() {
let mockApi;
let service;
beforeEach(function() {
mockApi = jasmine.createSpyObj('Api', ['get', 'post']);
service = new Service(mockApi);
});
it('should use mock', function() {
mockApi.get.and.returnValue('mock data');
expect(service.getData()).toBe('mock data');
});
});
2. 清理 Spy 状态 #
javascript
describe('cleaning spy state', function() {
let spy;
beforeEach(function() {
spy = jasmine.createSpy('spy');
});
afterEach(function() {
spy.calls.reset();
});
it('test 1', function() {
spy('a');
expect(spy).toHaveBeenCalled();
});
it('test 2 - spy is clean', function() {
expect(spy).not.toHaveBeenCalled();
});
});
3. 使用 jasmine.any 进行灵活匹配 #
javascript
describe('flexible matching', function() {
it('should match any function', function() {
const spy = jasmine.createSpy('spy');
spy(function() {});
expect(spy).toHaveBeenCalledWith(jasmine.any(Function));
});
it('should match any number', function() {
const spy = jasmine.createSpy('spy');
spy(42);
expect(spy).toHaveBeenCalledWith(jasmine.any(Number));
});
});
Spy 对照表 #
| 方法 | 用途 | 示例 |
|---|---|---|
| spyOn | 监视对象方法 | spyOn(obj, ‘method’) |
| spyOnProperty | 监视属性 | spyOnProperty(obj, ‘prop’, ‘get’) |
| createSpy | 创建独立 Spy | jasmine.createSpy(‘name’) |
| createSpyObj | 创建 Spy 对象 | jasmine.createSpyObj(‘Obj’, [‘method1’, ‘method2’]) |
| and.returnValue | 设置返回值 | spy.and.returnValue(value) |
| and.returnValues | 设置多次返回值 | spy.and.returnValues(v1, v2) |
| and.callThrough | 调用原方法 | spy.and.callThrough() |
| and.callFake | 使用假函数 | spy.and.callFake(fn) |
| and.throwError | 抛出错误 | spy.and.throwError(‘error’) |
| and.stub | 恢复为空函数 | spy.and.stub() |
下一步 #
现在你已经掌握了 Jasmine 的 Spy 功能,接下来学习 配置选项 了解如何配置 Jasmine!
最后更新:2026-03-28