Jasmine 高级特性 #
高级特性概览 #
本章介绍 Jasmine 的高级功能,帮助你成为测试专家:
text
┌─────────────────────────────────────────────────────────────┐
│ 高级特性概览 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 自定义匹配器 │ │ 时钟模拟 │ │ 测试数据管理 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 异步进阶 │ │ 调试技巧 │ │ 最佳实践 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
自定义匹配器 #
基本自定义匹配器 #
javascript
describe('custom matchers', function() {
beforeEach(function() {
jasmine.addMatchers({
toBeEven: function() {
return {
compare: function(actual) {
const result = {};
result.pass = actual % 2 === 0;
if (result.pass) {
result.message = `Expected ${actual} not to be even`;
} else {
result.message = `Expected ${actual} to be even`;
}
return result;
}
};
}
});
});
it('should check even numbers', function() {
expect(4).toBeEven();
expect(3).not.toBeEven();
expect(0).toBeEven();
});
});
带参数的自定义匹配器 #
javascript
describe('parameterized matchers', function() {
beforeEach(function() {
jasmine.addMatchers({
toBeWithinRange: function() {
return {
compare: function(actual, min, max) {
const result = {};
result.pass = actual >= min && actual <= max;
if (result.pass) {
result.message = `Expected ${actual} not to be within ${min} and ${max}`;
} else {
result.message = `Expected ${actual} to be within ${min} and ${max}`;
}
return result;
}
};
},
toHaveLength: function() {
return {
compare: function(actual, expected) {
const result = {};
result.pass = actual.length === expected;
if (result.pass) {
result.message = `Expected ${JSON.stringify(actual)} not to have length ${expected}`;
} else {
result.message = `Expected ${JSON.stringify(actual)} to have length ${expected}, but got ${actual.length}`;
}
return result;
}
};
}
});
});
it('should check range', function() {
expect(5).toBeWithinRange(1, 10);
expect(15).not.toBeWithinRange(1, 10);
});
it('should check length', function() {
expect([1, 2, 3]).toHaveLength(3);
expect('hello').toHaveLength(5);
});
});
否定断言的自定义匹配器 #
javascript
describe('negation matchers', function() {
beforeEach(function() {
jasmine.addMatchers({
toBeValidEmail: function() {
return {
compare: function(actual) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const result = {};
result.pass = emailRegex.test(actual);
if (result.pass) {
result.message = `Expected "${actual}" not to be a valid email`;
} else {
result.message = `Expected "${actual}" to be a valid email`;
}
return result;
}
};
}
});
});
it('should validate emails', function() {
expect('test@example.com').toBeValidEmail();
expect('invalid-email').not.toBeValidEmail();
expect('user.name@domain.co.uk').toBeValidEmail();
});
});
全局注册自定义匹配器 #
javascript
// spec/helpers/custom-matchers.js
const customMatchers = {
toBeValidEmail: function() {
return {
compare: function(actual) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return {
pass: emailRegex.test(actual),
message: `Expected "${actual}" ${emailRegex.test(actual) ? 'not ' : ''}to be a valid email`
};
}
};
},
toBeValidPhone: function() {
return {
compare: function(actual) {
const phoneRegex = /^\d{3}-\d{3}-\d{4}$/;
return {
pass: phoneRegex.test(actual),
message: `Expected "${actual}" ${phoneRegex.test(actual) ? 'not ' : ''}to be a valid phone number`
};
}
};
},
toBeValidUrl: function() {
return {
compare: function(actual) {
try {
new URL(actual);
return { pass: true, message: `Expected "${actual}" not to be a valid URL` };
} catch (e) {
return { pass: false, message: `Expected "${actual}" to be a valid URL` };
}
}
};
}
};
beforeEach(function() {
jasmine.addMatchers(customMatchers);
});
时钟模拟进阶 #
jasmine.clock() 详细用法 #
javascript
describe('jasmine.clock advanced', function() {
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
describe('setTimeout', function() {
it('should execute callback after delay', function() {
let executed = false;
setTimeout(function() {
executed = true;
}, 1000);
expect(executed).toBe(false);
jasmine.clock().tick(500);
expect(executed).toBe(false);
jasmine.clock().tick(500);
expect(executed).toBe(true);
});
it('should handle multiple timers', function() {
const results = [];
setTimeout(function() { results.push('first'); }, 100);
setTimeout(function() { results.push('second'); }, 200);
setTimeout(function() { results.push('third'); }, 300);
jasmine.clock().tick(100);
expect(results).toEqual(['first']);
jasmine.clock().tick(100);
expect(results).toEqual(['first', 'second']);
jasmine.clock().tick(100);
expect(results).toEqual(['first', 'second', 'third']);
});
});
describe('setInterval', function() {
it('should execute repeatedly', function() {
let counter = 0;
const intervalId = setInterval(function() {
counter++;
}, 100);
jasmine.clock().tick(100);
expect(counter).toBe(1);
jasmine.clock().tick(100);
expect(counter).toBe(2);
jasmine.clock().tick(300);
expect(counter).toBe(5);
clearInterval(intervalId);
jasmine.clock().tick(100);
expect(counter).toBe(5);
});
});
describe('Date mocking', function() {
it('should mock current date', function() {
const baseTime = new Date(2024, 0, 1, 12, 0, 0);
jasmine.clock().mockDate(baseTime);
expect(new Date().getTime()).toBe(baseTime.getTime());
jasmine.clock().tick(1000);
expect(new Date().getTime()).toBe(baseTime.getTime() + 1000);
jasmine.clock().tick(60000);
expect(new Date().getMinutes()).toBe(1);
});
});
});
实际应用:防抖和节流 #
javascript
describe('debounce and throttle', function() {
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
describe('debounce', function() {
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
it('should debounce function calls', function() {
const spy = jasmine.createSpy('fn');
const debouncedFn = debounce(spy, 100);
debouncedFn('first');
debouncedFn('second');
debouncedFn('third');
expect(spy).not.toHaveBeenCalled();
jasmine.clock().tick(100);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('third');
});
});
describe('throttle', function() {
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
it('should throttle function calls', function() {
const spy = jasmine.createSpy('fn');
const throttledFn = throttle(spy, 100);
throttledFn('first');
expect(spy).toHaveBeenCalledTimes(1);
throttledFn('second');
throttledFn('third');
expect(spy).toHaveBeenCalledTimes(1);
jasmine.clock().tick(100);
throttledFn('fourth');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith('fourth');
});
});
});
测试数据管理 #
使用工厂函数 #
javascript
// spec/helpers/factories.js
function createUser(overrides = {}) {
return {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30,
role: 'user',
...overrides
};
}
function createProduct(overrides = {}) {
return {
id: 1,
name: 'Product Name',
price: 100,
stock: 10,
category: 'electronics',
...overrides
};
}
function createOrder(overrides = {}) {
return {
id: 'order-1',
userId: 1,
items: [
{ productId: 1, quantity: 2, price: 100 }
],
status: 'pending',
total: 200,
...overrides
};
}
module.exports = { createUser, createProduct, createOrder };
使用工厂函数测试 #
javascript
const { createUser, createProduct, createOrder } = require('./helpers/factories');
describe('OrderService', function() {
let orderService;
beforeEach(function() {
orderService = new OrderService();
});
describe('calculateTotal', function() {
it('should calculate order total', function() {
const order = createOrder({
items: [
{ productId: 1, quantity: 2, price: 100 },
{ productId: 2, quantity: 1, price: 50 }
]
});
const total = orderService.calculateTotal(order);
expect(total).toBe(250);
});
});
describe('validateOrder', function() {
it('should validate user age', function() {
const user = createUser({ age: 15 });
const order = createOrder({ userId: user.id });
expect(function() {
orderService.validateOrder(order, user);
}).toThrowError('User must be 18 or older');
});
it('should validate product stock', function() {
const user = createUser();
const product = createProduct({ stock: 0 });
const order = createOrder({
items: [{ productId: product.id, quantity: 1 }]
});
expect(function() {
orderService.validateOrder(order, user, [product]);
}).toThrowError('Product out of stock');
});
});
});
使用 Builder 模式 #
javascript
class UserBuilder {
constructor() {
this.user = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30,
role: 'user'
};
}
withId(id) {
this.user.id = id;
return this;
}
withName(name) {
this.user.name = name;
return this;
}
withEmail(email) {
this.user.email = email;
return this;
}
withAge(age) {
this.user.age = age;
return this;
}
asAdmin() {
this.user.role = 'admin';
return this;
}
build() {
return { ...this.user };
}
}
describe('UserBuilder', function() {
it('should build user with custom properties', function() {
const user = new UserBuilder()
.withName('Jane')
.withAge(25)
.asAdmin()
.build();
expect(user.name).toBe('Jane');
expect(user.age).toBe(25);
expect(user.role).toBe('admin');
});
});
调试技巧 #
使用 console.log #
javascript
describe('debugging', function() {
it('should debug with console.log', function() {
const data = { name: 'John', age: 30 };
console.log('Data:', data);
console.log('JSON:', JSON.stringify(data, null, 2));
expect(data.name).toBe('John');
});
});
使用 jasmine.anything() #
javascript
describe('partial matching', function() {
it('should match anything', function() {
const spy = jasmine.createSpy('spy');
spy({ id: 1, name: 'John', timestamp: Date.now() });
expect(spy).toHaveBeenCalledWith({
id: 1,
name: 'John',
timestamp: jasmine.anything()
});
});
});
使用 jasmine.objectContaining() #
javascript
describe('object matching', function() {
it('should match partial object', function() {
const result = {
id: 1,
name: 'John',
email: 'john@example.com',
createdAt: '2024-01-01',
updatedAt: '2024-01-02'
};
expect(result).toEqual(jasmine.objectContaining({
id: 1,
name: 'John'
}));
});
});
使用 jasmine.arrayContaining() #
javascript
describe('array matching', function() {
it('should match partial array', function() {
const result = [1, 2, 3, 4, 5];
expect(result).toEqual(jasmine.arrayContaining([1, 3, 5]));
expect(result).not.toEqual(jasmine.arrayContaining([6]));
});
});
使用 jasmine.stringMatching() #
javascript
describe('string matching', function() {
it('should match string pattern', function() {
const message = 'Error: User not found with id 123';
expect(message).toEqual(jasmine.stringMatching(/Error:/));
expect(message).toEqual(jasmine.stringMatching(/id \d+/));
});
});
最佳实践 #
1. 测试命名规范 #
javascript
describe('UserService', function() {
describe('createUser', function() {
it('should create user with valid data', function() {});
it('should throw error when email is invalid', function() {});
it('should throw error when name is empty', function() {});
});
describe('updateUser', function() {
it('should update user name', function() {});
it('should throw error when user not found', function() {});
});
});
2. 测试隔离 #
javascript
describe('isolated tests', function() {
let service;
let mockDb;
beforeEach(function() {
mockDb = jasmine.createSpyObj('Database', ['find', 'save', 'delete']);
service = new UserService(mockDb);
});
afterEach(function() {
});
it('should be isolated from other tests', function() {
service.createUser({ name: 'John' });
expect(mockDb.save).toHaveBeenCalled();
});
});
3. 使用 describe 组织测试 #
javascript
describe('Calculator', function() {
describe('arithmetic operations', function() {
describe('addition', function() {
it('should add positive numbers', function() {});
it('should add negative numbers', function() {});
});
describe('subtraction', function() {
it('should subtract positive numbers', function() {});
it('should subtract negative numbers', function() {});
});
});
describe('edge cases', function() {
it('should handle zero', function() {});
it('should handle large numbers', function() {});
});
});
4. 避免测试实现细节 #
javascript
describe('UserService', function() {
// ✅ 测试行为
it('should return user when found', async function() {
const user = await userService.getUser(1);
expect(user).toBeDefined();
expect(user.id).toBe(1);
});
// ❌ 避免测试实现细节
it('should call internal method', function() {
spyOn(userService, '_internalMethod');
userService.getUser(1);
expect(userService._internalMethod).toHaveBeenCalled();
});
});
5. 使用 AAA 模式 #
javascript
describe('AAA pattern', function() {
it('should add item to cart', function() {
// Arrange
const cart = new ShoppingCart();
const item = { id: 1, name: 'Product', price: 100 };
// Act
cart.addItem(item);
// Assert
expect(cart.items.length).toBe(1);
expect(cart.total).toBe(100);
});
});
6. 测试边界条件 #
javascript
describe('boundary testing', function() {
describe('divide', function() {
it('should divide positive numbers', function() {
expect(divide(10, 2)).toBe(5);
});
it('should handle division by zero', function() {
expect(function() { divide(1, 0); }).toThrow();
});
it('should handle negative numbers', function() {
expect(divide(-10, 2)).toBe(-5);
});
it('should handle decimal results', function() {
expect(divide(10, 3)).toBeCloseTo(3.333, 2);
});
it('should handle very large numbers', function() {
expect(divide(Number.MAX_SAFE_INTEGER, 1)).toBe(Number.MAX_SAFE_INTEGER);
});
});
});
7. 清理测试状态 #
javascript
describe('state cleanup', function() {
let container;
beforeEach(function() {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(function() {
document.body.removeChild(container);
});
it('should test DOM manipulation', function() {
container.innerHTML = '<span>test</span>';
expect(container.children.length).toBe(1);
});
});
性能优化 #
1. 减少重复设置 #
javascript
describe('optimization', function() {
let sharedResource;
beforeAll(function() {
sharedResource = createExpensiveResource();
});
afterAll(function() {
sharedResource.destroy();
});
it('should use shared resource', function() {
expect(sharedResource.isReady()).toBe(true);
});
});
2. 使用 test.each 替代重复测试 #
javascript
describe('parameterized tests', function() {
const testCases = [
{ input: 1, expected: 1 },
{ input: 2, expected: 4 },
{ input: 3, expected: 9 },
{ input: 4, expected: 16 }
];
testCases.forEach(function({ input, expected }) {
it(`square(${input}) should return ${expected}`, function() {
expect(square(input)).toBe(expected);
});
});
});
常见问题解决 #
1. 测试超时 #
javascript
describe('timeout issues', function() {
it('should handle long operations', async function() {
const result = await longRunningOperation();
expect(result).toBeDefined();
}, 10000);
it('should not forget done callback', function(done) {
asyncFunction(function(result) {
expect(result).toBeDefined();
done();
});
});
});
2. Spy 未正确设置 #
javascript
describe('spy issues', function() {
it('should spy before calling', function() {
const obj = {
method: function() { return 'original'; }
};
spyOn(obj, 'method').and.returnValue('mocked');
expect(obj.method()).toBe('mocked');
});
it('should restore original after test', function() {
const obj = {
method: function() { return 'original'; }
};
spyOn(obj, 'method').and.callThrough();
expect(obj.method()).toBe('original');
});
});
3. 异步测试问题 #
javascript
describe('async issues', function() {
it('should return promise', function() {
return asyncFunction().then(function(result) {
expect(result).toBeDefined();
});
});
it('should use async/await', async function() {
const result = await asyncFunction();
expect(result).toBeDefined();
});
});
总结 #
通过学习 Jasmine 的高级特性,你已经掌握了:
- 自定义匹配器 - 创建可复用的断言
- 时钟模拟 - 测试定时器相关代码
- 测试数据管理 - 使用工厂函数和 Builder 模式
- 调试技巧 - 使用各种匹配器进行调试
- 最佳实践 - 编写高质量测试的原则
继续实践这些技术,你将成为 Jasmine 测试专家!
最后更新:2026-03-28