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 的高级特性,你已经掌握了:

  1. 自定义匹配器 - 创建可复用的断言
  2. 时钟模拟 - 测试定时器相关代码
  3. 测试数据管理 - 使用工厂函数和 Builder 模式
  4. 调试技巧 - 使用各种匹配器进行调试
  5. 最佳实践 - 编写高质量测试的原则

继续实践这些技术,你将成为 Jasmine 测试专家!

最后更新:2026-03-28