Jasmine 异步测试 #

异步测试概述 #

JavaScript 中大量使用异步操作,Jasmine 提供了多种方式来测试异步代码:

text
┌─────────────────────────────────────────────────────────────┐
│                    异步测试方法                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │ done 回调    │  │ Promise     │  │ async/await │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐         │
│  │ 定时器模拟   │  │ 异步匹配器   │  │ 时钟控制    │         │
│  └─────────────┘  └─────────────┘  └─────────────┘         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

done 回调 #

基本用法 #

当测试需要等待异步操作完成时,使用 done 参数:

javascript
describe('async with done', function() {
  it('should wait for callback', function(done) {
    asyncFunction(function(result) {
      expect(result).toBe('success');
      done();
    });
  });
});

超时设置 #

javascript
describe('timeout', function() {
  it('should complete within default timeout', function(done) {
    setTimeout(function() {
      expect(true).toBe(true);
      done();
    }, 4000);
  });

  it('should complete within custom timeout', function(done) {
    setTimeout(function() {
      expect(true).toBe(true);
      done();
    }, 6000);
  }, 10000);
});

钩子函数中的 done #

javascript
describe('async hooks', function() {
  let data;

  beforeEach(function(done) {
    fetchData(function(result) {
      data = result;
      done();
    });
  });

  afterEach(function(done) {
    cleanup(function() {
      done();
    });
  });

  it('should have data', function() {
    expect(data).toBeDefined();
  });
});

错误处理 #

javascript
describe('error handling', function() {
  it('should handle async error', function(done) {
    asyncFunction(function(error, result) {
      if (error) {
        expect(error).not.toBeNull();
        done();
        return;
      }
      expect(result).toBe('success');
      done();
    });
  });

  it('should catch thrown error', function(done) {
    expect(function() {
      asyncFunction(function(error) {
        if (error) {
          throw new Error('Async error');
        }
        done();
      });
    }).not.toThrow();

    setTimeout(function() {
      done();
    }, 100);
  });
});

Promise 测试 #

返回 Promise #

直接返回 Promise,Jasmine 会自动等待:

javascript
describe('Promise tests', function() {
  it('should handle resolved promise', function() {
    return fetchData().then(function(result) {
      expect(result).toBe('success');
    });
  });

  it('should handle rejected promise', function() {
    return fetchError().catch(function(error) {
      expect(error.message).toBe('Network error');
    });
  });
});

Promise 链 #

javascript
describe('Promise chain', function() {
  it('should chain promises', function() {
    return fetchUser(1)
      .then(function(user) {
        expect(user.name).toBe('John');
        return fetchOrders(user.id);
      })
      .then(function(orders) {
        expect(orders.length).toBeGreaterThan(0);
      });
  });
});

Promise.all #

javascript
describe('Promise.all', function() {
  it('should handle multiple promises', function() {
    return Promise.all([
      fetchUser(1),
      fetchUser(2)
    ]).then(function(users) {
      expect(users[0].name).toBe('John');
      expect(users[1].name).toBe('Jane');
    });
  });
});

async/await #

基本用法 #

javascript
describe('async/await', function() {
  it('should await promise', async function() {
    const result = await fetchData();
    expect(result).toBe('success');
  });

  it('should handle multiple awaits', async function() {
    const user = await fetchUser(1);
    const orders = await fetchOrders(user.id);
    expect(orders.length).toBeGreaterThan(0);
  });
});

错误处理 #

javascript
describe('async error handling', function() {
  it('should catch async error', async function() {
    try {
      await fetchError();
      fail('should have thrown');
    } catch (error) {
      expect(error.message).toBe('Network error');
    }
  });

  it('should use try/catch pattern', async function() {
    let error = null;
    try {
      await riskyOperation();
    } catch (e) {
      error = e;
    }
    expect(error).toBeNull();
  });
});

钩子函数中的 async/await #

javascript
describe('async hooks', function() {
  let db;

  beforeAll(async function() {
    db = await connectToDatabase();
  });

  afterAll(async function() {
    await db.close();
  });

  beforeEach(async function() {
    await db.clear();
  });

  it('should query database', async function() {
    const result = await db.query('SELECT 1');
    expect(result).toBeDefined();
  });
});

异步匹配器 #

toBeResolved / toBeRejected #

javascript
describe('async matchers', function() {
  it('should check resolved', async function() {
    await expectAsync(Promise.resolve('success')).toBeResolved();
  });

  it('should check rejected', async function() {
    await expectAsync(Promise.reject(new Error('failed'))).toBeRejected();
  });

  it('should check resolved value', async function() {
    await expectAsync(Promise.resolve(42)).toBeResolvedTo(42);
  });

  it('should check rejected with error type', async function() {
    await expectAsync(Promise.reject(new TypeError('error')))
      .toBeRejectedWithError(TypeError);
  });

  it('should check rejected with error message', async function() {
    await expectAsync(Promise.reject(new Error('not found')))
      .toBeRejectedWithError(Error, 'not found');
  });

  it('should check rejected with regex', async function() {
    await expectAsync(Promise.reject(new Error('User not found')))
      .toBeRejectedWithError(Error, /not found/);
  });
});

异步匹配器组合 #

javascript
describe('async matcher combinations', function() {
  it('should resolve with specific value', async function() {
    const promise = fetchUser(1);
    await expectAsync(promise).toBeResolved();
    const user = await promise;
    expect(user.name).toBe('John');
  });

  it('should reject with specific error', async function() {
    const promise = fetchUser(-1);
    await expectAsync(promise).toBeRejected();
    try {
      await promise;
    } catch (error) {
      expect(error.message).toBe('Invalid user ID');
    }
  });
});

定时器测试 #

jasmine.clock() #

使用 jasmine.clock() 模拟时间:

javascript
describe('timer tests', function() {
  beforeEach(function() {
    jasmine.clock().install();
  });

  afterEach(function() {
    jasmine.clock().uninstall();
  });

  it('should execute after delay', function() {
    let executed = false;

    setTimeout(function() {
      executed = true;
    }, 1000);

    expect(executed).toBe(false);

    jasmine.clock().tick(1000);

    expect(executed).toBe(true);
  });

  it('should handle setInterval', function() {
    let counter = 0;

    setInterval(function() {
      counter++;
    }, 100);

    expect(counter).toBe(0);

    jasmine.clock().tick(100);
    expect(counter).toBe(1);

    jasmine.clock().tick(200);
    expect(counter).toBe(3);
  });
});

模拟 Date #

javascript
describe('mocking Date', function() {
  beforeEach(function() {
    jasmine.clock().install();
  });

  afterEach(function() {
    jasmine.clock().uninstall();
  });

  it('should mock current date', function() {
    const baseTime = new Date(2024, 0, 1);
    jasmine.clock().mockDate(baseTime);

    expect(new Date().getTime()).toBe(baseTime.getTime());

    jasmine.clock().tick(1000);
    expect(new Date().getTime()).toBe(baseTime.getTime() + 1000);
  });
});

实际应用示例 #

javascript
describe('Timer class', function() {
  let timer;
  let callback;

  beforeEach(function() {
    jasmine.clock().install();
    callback = jasmine.createSpy('callback');
    timer = new Timer(callback, 1000);
  });

  afterEach(function() {
    timer.stop();
    jasmine.clock().uninstall();
  });

  it('should call callback after delay', function() {
    timer.start();
    expect(callback).not.toHaveBeenCalled();

    jasmine.clock().tick(1000);
    expect(callback).toHaveBeenCalled();
  });

  it('should not call callback if stopped', function() {
    timer.start();
    jasmine.clock().tick(500);
    timer.stop();
    jasmine.clock().tick(500);

    expect(callback).not.toHaveBeenCalled();
  });
});

异步测试模式 #

轮询等待 #

javascript
describe('polling pattern', function() {
  function waitForCondition(condition, timeout = 5000) {
    return new Promise(function(resolve, reject) {
      const startTime = Date.now();

      function check() {
        if (condition()) {
          resolve();
        } else if (Date.now() - startTime > timeout) {
          reject(new Error('Timeout waiting for condition'));
        } else {
          setTimeout(check, 100);
        }
      }

      check();
    });
  }

  it('should wait for condition', async function() {
    let ready = false;

    setTimeout(function() {
      ready = true;
    }, 500);

    await waitForCondition(function() {
      return ready;
    });

    expect(ready).toBe(true);
  });
});

重试模式 #

javascript
describe('retry pattern', function() {
  async function retry(fn, maxAttempts = 3, delay = 100) {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await fn();
      } catch (error) {
        if (attempt === maxAttempts) {
          throw error;
        }
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  it('should retry on failure', async function() {
    let attempts = 0;

    const result = await retry(async function() {
      attempts++;
      if (attempts < 3) {
        throw new Error('Not ready');
      }
      return 'success';
    });

    expect(result).toBe('success');
    expect(attempts).toBe(3);
  });
});

实际场景示例 #

API 请求测试 #

javascript
describe('API client', function() {
  let api;
  let mockFetch;

  beforeEach(function() {
    mockFetch = jasmine.createSpy('fetch');
    api = new ApiClient(mockFetch);
  });

  it('should fetch user', async function() {
    mockFetch.and.returnValue(Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ id: 1, name: 'John' })
    }));

    const user = await api.getUser(1);

    expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
    expect(user.name).toBe('John');
  });

  it('should handle network error', async function() {
    mockFetch.and.returnValue(Promise.reject(new Error('Network error')));

    try {
      await api.getUser(1);
      fail('should have thrown');
    } catch (error) {
      expect(error.message).toBe('Network error');
    }
  });

  it('should handle HTTP error', async function() {
    mockFetch.and.returnValue(Promise.resolve({
      ok: false,
      status: 404,
      statusText: 'Not Found'
    }));

    try {
      await api.getUser(1);
      fail('should have thrown');
    } catch (error) {
      expect(error.message).toContain('404');
    }
  });
});

数据库操作测试 #

javascript
describe('Database operations', function() {
  let db;

  beforeAll(async function() {
    db = await connectToTestDatabase();
  });

  afterAll(async function() {
    await db.disconnect();
  });

  beforeEach(async function() {
    await db.clear();
  });

  describe('User operations', function() {
    it('should create user', async function() {
      const user = await db.users.create({
        name: 'John',
        email: 'john@example.com'
      });

      expect(user.id).toBeDefined();
      expect(user.name).toBe('John');
    });

    it('should find user by id', async function() {
      const created = await db.users.create({ name: 'John' });
      const found = await db.users.findById(created.id);

      expect(found).toEqual(created);
    });

    it('should update user', async function() {
      const user = await db.users.create({ name: 'John' });
      const updated = await db.users.update(user.id, { name: 'Jane' });

      expect(updated.name).toBe('Jane');
    });

    it('should delete user', async function() {
      const user = await db.users.create({ name: 'John' });
      await db.users.delete(user.id);

      const found = await db.users.findById(user.id);
      expect(found).toBeNull();
    });
  });
});

事件监听测试 #

javascript
describe('EventEmitter', function() {
  let emitter;

  beforeEach(function() {
    emitter = new EventEmitter();
  });

  it('should emit and receive events', function(done) {
    emitter.on('message', function(data) {
      expect(data).toBe('hello');
      done();
    });

    emitter.emit('message', 'hello');
  });

  it('should handle multiple listeners', function(done) {
    let count = 0;

    emitter.on('event', function() {
      count++;
      if (count === 2) {
        done();
      }
    });

    emitter.on('event', function() {
      count++;
      if (count === 2) {
        done();
      }
    });

    emitter.emit('event');
  });

  it('should remove listener', function() {
    const listener = jasmine.createSpy('listener');
    emitter.on('event', listener);
    emitter.off('event', listener);
    emitter.emit('event');

    expect(listener).not.toHaveBeenCalled();
  });
});

最佳实践 #

1. 选择合适的异步方式 #

javascript
// ✅ 推荐 - async/await(最清晰)
it('should fetch data', async function() {
  const data = await fetchData();
  expect(data).toBeDefined();
});

// ✅ 可接受 - 返回 Promise
it('should fetch data', function() {
  return fetchData().then(function(data) {
    expect(data).toBeDefined();
  });
});

// ⚠️ 谨慎使用 - done 回调(较繁琐)
it('should fetch data', function(done) {
  fetchData(function(data) {
    expect(data).toBeDefined();
    done();
  });
});

2. 合理设置超时 #

javascript
describe('timeout settings', function() {
  it('should use default timeout', async function() {
    const result = await quickOperation();
    expect(result).toBeDefined();
  });

  it('should use longer timeout for slow operations', async function() {
    const result = await slowOperation();
    expect(result).toBeDefined();
  }, 10000);
});

3. 正确清理资源 #

javascript
describe('resource cleanup', function() {
  let server;

  beforeEach(function() {
    server = startServer();
  });

  afterEach(async function() {
    await server.stop();
  });

  it('should handle requests', async function() {
    const response = await fetch('http://localhost:3000/api');
    expect(response.ok).toBe(true);
  });
});

常见问题 #

1. 忘记返回 Promise #

javascript
// ❌ 错误 - 测试会立即结束
it('should work', function() {
  fetchData().then(function(data) {
    expect(data).toBeDefined();
  });
});

// ✅ 正确 - 返回 Promise
it('should work', function() {
  return fetchData().then(function(data) {
    expect(data).toBeDefined();
  });
});

2. 未处理的 Promise 拒绝 #

javascript
// ❌ 错误 - 未处理拒绝
it('should handle error', function() {
  fetchError();
});

// ✅ 正确 - 捕获拒绝
it('should handle error', async function() {
  try {
    await fetchError();
    fail('should have thrown');
  } catch (error) {
    expect(error).toBeDefined();
  }
});

下一步 #

现在你已经掌握了 Jasmine 的异步测试方法,接下来学习 Spy 功能 了解如何使用测试替身!

最后更新:2026-03-28