name: Jasmine Testing description: BDD-style JavaScript testing with Jasmine covering spies, async patterns, custom matchers, clock manipulation, and comprehensive test organization for frontend and Node.js applications. version: 1.0.0 author: thetestingacademy license: MIT tags: [jasmine, bdd, javascript, spies, async-testing, matchers, unit-testing, typescript] testingTypes: [unit, integration] frameworks: [jasmine] languages: [javascript, typescript] domains: [web, api, backend] agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt]
Jasmine Testing Skill
You are an expert software engineer specializing in BDD-style testing with Jasmine. When the user asks you to write, review, or debug Jasmine tests, follow these detailed instructions to produce production-grade test suites that are readable, maintainable, and comprehensive.
Core Principles
- Behavior-Driven Development -- Write specs that describe behavior from the user's perspective using
describe,it, andexpectin natural language. - One expectation focus per spec -- Each
itblock should verify a single logical behavior to make failures easy to diagnose. - Arrange-Act-Assert -- Structure every spec into setup, execution, and verification phases even when using
beforeEach. - Isolate with spies -- Use
jasmine.createSpy()andjasmine.createSpyObj()to eliminate external dependencies and side effects. - Descriptive spec names -- Spec names should read as complete sentences:
it('should return the sum of two positive numbers'). - Clean up after yourself -- Always uninstall clocks, restore spies, and tear down DOM modifications in
afterEachblocks. - Prefer async/await -- Use modern async patterns over
done()callbacks for cleaner, more readable async specs.
Project Structure
src/
services/
user.service.js
user.service.spec.js
payment.service.js
payment.service.spec.js
utils/
validators.js
validators.spec.js
formatters.js
formatters.spec.js
models/
user.model.js
user.model.spec.js
helpers/
jasmine-helpers.js
spec/
support/
jasmine.json
integration/
user-payment.spec.js
Configuration
jasmine.json
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true,
"forbidDuplicateNames": true
}
}
package.json Setup
{
"devDependencies": {
"jasmine": "^5.1.0",
"@types/jasmine": "^5.1.0"
},
"scripts": {
"test": "jasmine",
"test:watch": "nodemon --exec jasmine",
"test:coverage": "c8 jasmine"
}
}
Basic Test Structure
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
afterEach(() => {
calculator = null;
});
describe('add', () => {
it('should return the sum of two positive numbers', () => {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('should handle negative numbers', () => {
const result = calculator.add(-1, -3);
expect(result).toBe(-4);
});
it('should handle zero', () => {
const result = calculator.add(0, 5);
expect(result).toBe(5);
});
});
describe('divide', () => {
it('should return the quotient of two numbers', () => {
const result = calculator.divide(10, 2);
expect(result).toBe(5);
});
it('should throw an error when dividing by zero', () => {
expect(() => calculator.divide(10, 0)).toThrowError('Division by zero');
});
});
});
Spy Patterns
Creating Spies
describe('UserService', () => {
let userService;
let apiClient;
beforeEach(() => {
apiClient = jasmine.createSpyObj('ApiClient', ['get', 'post', 'put', 'delete']);
userService = new UserService(apiClient);
});
it('should fetch user by ID', async () => {
const mockUser = { id: 1, name: 'Alice' };
apiClient.get.and.returnValue(Promise.resolve(mockUser));
const user = await userService.getUser(1);
expect(apiClient.get).toHaveBeenCalledWith('/users/1');
expect(apiClient.get).toHaveBeenCalledTimes(1);
expect(user).toEqual(mockUser);
});
it('should create a new user', async () => {
const newUser = { name: 'Bob', email: 'bob@example.com' };
const savedUser = { id: 2, ...newUser };
apiClient.post.and.returnValue(Promise.resolve(savedUser));
const result = await userService.createUser(newUser);
expect(apiClient.post).toHaveBeenCalledWith('/users', newUser);
expect(result.id).toBe(2);
});
});
Spying on Existing Methods
describe('EventLogger', () => {
let logger;
beforeEach(() => {
logger = new EventLogger();
spyOn(logger, 'sendToServer').and.callFake(() => Promise.resolve());
spyOn(console, 'error');
});
it('should log events and send to server', async () => {
await logger.logEvent('click', { button: 'submit' });
expect(logger.sendToServer).toHaveBeenCalledWith(
jasmine.objectContaining({
type: 'click',
data: { button: 'submit' },
timestamp: jasmine.any(Number)
})
);
});
it('should handle server failure gracefully', async () => {
logger.sendToServer.and.returnValue(Promise.reject(new Error('Network error')));
await logger.logEvent('click', { button: 'submit' });
expect(console.error).toHaveBeenCalledWith(
'Failed to send event:',
jasmine.any(Error)
);
});
});
Async Testing Patterns
Using async/await
describe('DataFetcher', () => {
let fetcher;
beforeEach(() => {
fetcher = new DataFetcher();
});
it('should fetch and transform data', async () => {
spyOn(fetcher, 'fetchRaw').and.returnValue(
Promise.resolve({ items: [{ id: 1 }, { id: 2 }] })
);
const result = await fetcher.getTransformedData();
expect(result).toEqual([
jasmine.objectContaining({ id: 1 }),
jasmine.objectContaining({ id: 2 })
]);
});
it('should retry on failure', async () => {
let callCount = 0;
spyOn(fetcher, 'fetchRaw').and.callFake(() => {
callCount++;
if (callCount < 3) {
return Promise.reject(new Error('Temporary failure'));
}
return Promise.resolve({ items: [] });
});
const result = await fetcher.getTransformedData();
expect(fetcher.fetchRaw).toHaveBeenCalledTimes(3);
expect(result).toEqual([]);
});
});
Clock Manipulation
describe('SessionManager', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should expire session after 30 minutes', () => {
const session = new SessionManager();
session.start();
expect(session.isActive()).toBe(true);
jasmine.clock().tick(30 * 60 * 1000);
expect(session.isActive()).toBe(false);
});
it('should refresh session on activity', () => {
const session = new SessionManager();
session.start();
jasmine.clock().tick(20 * 60 * 1000);
session.recordActivity();
jasmine.clock().tick(20 * 60 * 1000);
expect(session.isActive()).toBe(true);
});
});
Matcher Reference
Built-in Matchers
describe('Matcher examples', () => {
it('demonstrates equality matchers', () => {
expect(1 + 1).toBe(2);
expect({ a: 1 }).toEqual({ a: 1 });
expect(undefined).toBeUndefined();
expect(null).toBeNull();
expect('hello').toBeDefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy();
});
it('demonstrates comparison matchers', () => {
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThan(10);
expect(10).toBeGreaterThanOrEqual(10);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});
it('demonstrates string matchers', () => {
expect('hello world').toContain('world');
expect('hello world').toMatch(/^hello/);
});
it('demonstrates array matchers', () => {
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveSize(3);
});
it('demonstrates object matchers', () => {
const user = { name: 'Alice', age: 30, role: 'admin' };
expect(user).toEqual(jasmine.objectContaining({ name: 'Alice' }));
expect(user.name).toEqual(jasmine.stringContaining('Ali'));
});
it('demonstrates exception matchers', () => {
const badFn = () => { throw new TypeError('invalid type'); };
expect(badFn).toThrow();
expect(badFn).toThrowError(TypeError);
expect(badFn).toThrowError('invalid type');
});
});
Custom Matchers
beforeEach(() => {
jasmine.addMatchers({
toBeValidEmail: () => ({
compare: (actual) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(actual);
return {
pass,
message: pass
? `Expected ${actual} not to be a valid email`
: `Expected ${actual} to be a valid email`
};
}
}),
toBeWithinRange: () => ({
compare: (actual, floor, ceiling) => {
const pass = actual >= floor && actual <= ceiling;
return {
pass,
message: `Expected ${actual} to be within range [${floor}, ${ceiling}]`
};
}
})
});
});
describe('Custom matcher usage', () => {
it('should validate email format', () => {
expect('user@example.com').toBeValidEmail();
expect('invalid-email').not.toBeValidEmail();
});
it('should check value ranges', () => {
expect(5).toBeWithinRange(1, 10);
expect(15).not.toBeWithinRange(1, 10);
});
});
Nested Describe Blocks for Organization
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart();
});
describe('when empty', () => {
it('should have zero items', () => {
expect(cart.itemCount()).toBe(0);
});
it('should have zero total', () => {
expect(cart.total()).toBe(0);
});
});
describe('when adding items', () => {
beforeEach(() => {
cart.addItem({ name: 'Widget', price: 9.99, quantity: 2 });
});
it('should update item count', () => {
expect(cart.itemCount()).toBe(2);
});
it('should calculate total correctly', () => {
expect(cart.total()).toBeCloseTo(19.98, 2);
});
describe('and applying a discount', () => {
it('should reduce total by discount percentage', () => {
cart.applyDiscount(0.1);
expect(cart.total()).toBeCloseTo(17.98, 2);
});
});
});
describe('when removing items', () => {
beforeEach(() => {
cart.addItem({ name: 'Widget', price: 9.99, quantity: 2 });
cart.addItem({ name: 'Gadget', price: 14.99, quantity: 1 });
});
it('should remove the specified item', () => {
cart.removeItem('Widget');
expect(cart.itemCount()).toBe(1);
});
it('should throw if item not found', () => {
expect(() => cart.removeItem('NonExistent')).toThrowError('Item not found');
});
});
});
Best Practices
- Use
beforeEachfor shared setup -- Avoid duplicating setup code across specs; put common initialization inbeforeEachblocks for consistency and DRY code. - Always uninstall Jasmine clock -- If you call
jasmine.clock().install(), always pair it withjasmine.clock().uninstall()inafterEachto prevent cross-spec contamination. - Use
jasmine.objectContainingfor partial matches -- When testing objects with dynamic fields like timestamps or IDs, match only the fields you care about. - Prefer
createSpyObjover manual mocks -- It creates a clean mock with typed spy methods and avoids accidentally calling real implementations. - Test error paths explicitly -- Every function that can throw or reject should have specs for each error scenario.
- Randomize spec execution order -- Set
random: truein jasmine.json to catch specs that accidentally depend on execution order. - Use
fdescribeandfitonly during debugging -- Never commit focused specs to version control; they skip other tests silently. - Write descriptive failure messages -- Use custom matcher messages or add context to expectations so failures are self-documenting.
- Keep specs fast -- Unit specs should complete in under 50ms each. Move slow tests to a separate integration suite.
- Group related specs with nested
describeblocks -- Create a hierarchy that mirrors the conditions and behaviors being tested.
Anti-Patterns
- Testing implementation details -- Spying on private methods or asserting internal state creates brittle tests that break during refactoring without catching real bugs.
- Multiple unrelated assertions in one spec -- Combining unrelated checks in a single
itblock makes it impossible to identify which behavior failed. - Shared mutable state between specs -- Storing test state in variables outside
beforeEachcauses order-dependent failures that are difficult to debug. - Using
done()callback with async/await -- Mixing callback and promise patterns leads to confusing control flow and potential false positives. - Catching exceptions in specs -- Wrapping code in try/catch inside a spec swallows failures; use
toThrow()ortoThrowError()matchers instead. - Not restoring spies -- Forgetting to restore spied-on methods pollutes the global state for subsequent specs.
- Hardcoding test data inline -- Duplicating magic numbers and strings across specs makes maintenance painful; extract shared fixtures.
- Ignoring async rejection handling -- Not testing promise rejections means error paths go uncovered and may fail silently in production.
- Over-mocking -- Mocking every dependency including simple utility functions reduces test confidence; only mock I/O and non-deterministic code.
- Writing tests after the fact -- Retroactive tests tend to mirror implementation rather than specify behavior; practice TDD where possible.