by thetestingacademy
BDD-style JavaScript testing with Jasmine covering spies, async patterns, custom matchers, clock manipulation, and comprehensive test organization for frontend and Node.js applications.
npx @qaskills/cli add jasmine-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
describe, it, and expect in natural language.it block should verify a single logical behavior to make failures easy to diagnose.beforeEach.jasmine.createSpy() and jasmine.createSpyObj() to eliminate external dependencies and side effects.it('should return the sum of two positive numbers').afterEach blocks.done() callbacks for cleaner, more readable async specs.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
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true,
"forbidDuplicateNames": true
}
}
{
"devDependencies": {
"jasmine": "^5.1.0",
"@types/jasmine": "^5.1.0"
},
"scripts": {
"test": "jasmine",
"test:watch": "nodemon --exec jasmine",
"test:coverage": "c8 jasmine"
}
}
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');
});
});
});
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);
});
});
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)
);
});
});
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([]);
});
});
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);
});
});
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');
});
});
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);
});
});
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');
});
});
});
beforeEach for shared setup -- Avoid duplicating setup code across specs; put common initialization in beforeEach blocks for consistency and DRY code.jasmine.clock().install(), always pair it with jasmine.clock().uninstall() in afterEach to prevent cross-spec contamination.jasmine.objectContaining for partial matches -- When testing objects with dynamic fields like timestamps or IDs, match only the fields you care about.createSpyObj over manual mocks -- It creates a clean mock with typed spy methods and avoids accidentally calling real implementations.random: true in jasmine.json to catch specs that accidentally depend on execution order.fdescribe and fit only during debugging -- Never commit focused specs to version control; they skip other tests silently.describe blocks -- Create a hierarchy that mirrors the conditions and behaviors being tested.it block makes it impossible to identify which behavior failed.beforeEach causes order-dependent failures that are difficult to debug.done() callback with async/await -- Mixing callback and promise patterns leads to confusing control flow and potential false positives.toThrow() or toThrowError() matchers instead.- name: Install QA Skills
run: npx @qaskills/cli add jasmine-testing10 of 29 agents supported