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