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