BDD-style JavaScript testing with Jasmine covering spies, async patterns, custom matchers, clock manipulation, and comprehensive test organization for frontend and Node.js applications.
Scanned 6/4/2026
Install via CLI
openskills install PramodDutta/qaskills---
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
1. **Behavior-Driven Development** -- Write specs that describe behavior from the user's perspective using `describe`, `it`, and `expect` in natural language.
2. **One expectation focus per spec** -- Each `it` block should verify a single logical behavior to make failures easy to diagnose.
3. **Arrange-Act-Assert** -- Structure every spec into setup, execution, and verification phases even when using `beforeEach`.
4. **Isolate with spies** -- Use `jasmine.createSpy()` and `jasmine.createSpyObj()` to eliminate external dependencies and side effects.
5. **Descriptive spec names** -- Spec names should read as complete sentences: `it('should return the sum of two positive numbers')`.
6. **Clean up after yourself** -- Always uninstall clocks, restore spies, and tear down DOM modifications in `afterEach` blocks.
7. **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
```json
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true,
"forbidDuplicateNames": true
}
}
```
### package.json Setup
```json
{
"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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
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
1. **Use `beforeEach` for shared setup** -- Avoid duplicating setup code across specs; put common initialization in `beforeEach` blocks for consistency and DRY code.
2. **Always uninstall Jasmine clock** -- If you call `jasmine.clock().install()`, always pair it with `jasmine.clock().uninstall()` in `afterEach` to prevent cross-spec contamination.
3. **Use `jasmine.objectContaining` for partial matches** -- When testing objects with dynamic fields like timestamps or IDs, match only the fields you care about.
4. **Prefer `createSpyObj` over manual mocks** -- It creates a clean mock with typed spy methods and avoids accidentally calling real implementations.
5. **Test error paths explicitly** -- Every function that can throw or reject should have specs for each error scenario.
6. **Randomize spec execution order** -- Set `random: true` in jasmine.json to catch specs that accidentally depend on execution order.
7. **Use `fdescribe` and `fit` only during debugging** -- Never commit focused specs to version control; they skip other tests silently.
8. **Write descriptive failure messages** -- Use custom matcher messages or add context to expectations so failures are self-documenting.
9. **Keep specs fast** -- Unit specs should complete in under 50ms each. Move slow tests to a separate integration suite.
10. **Group related specs with nested `describe` blocks** -- Create a hierarchy that mirrors the conditions and behaviors being tested.
## Anti-Patterns
1. **Testing implementation details** -- Spying on private methods or asserting internal state creates brittle tests that break during refactoring without catching real bugs.
2. **Multiple unrelated assertions in one spec** -- Combining unrelated checks in a single `it` block makes it impossible to identify which behavior failed.
3. **Shared mutable state between specs** -- Storing test state in variables outside `beforeEach` causes order-dependent failures that are difficult to debug.
4. **Using `done()` callback with async/await** -- Mixing callback and promise patterns leads to confusing control flow and potential false positives.
5. **Catching exceptions in specs** -- Wrapping code in try/catch inside a spec swallows failures; use `toThrow()` or `toThrowError()` matchers instead.
6. **Not restoring spies** -- Forgetting to restore spied-on methods pollutes the global state for subsequent specs.
7. **Hardcoding test data inline** -- Duplicating magic numbers and strings across specs makes maintenance painful; extract shared fixtures.
8. **Ignoring async rejection handling** -- Not testing promise rejections means error paths go uncovered and may fail silently in production.
9. **Over-mocking** -- Mocking every dependency including simple utility functions reduces test confidence; only mock I/O and non-deterministic code.
10. **Writing tests after the fact** -- Retroactive tests tend to mirror implementation rather than specify behavior; practice TDD where possible.
No comments yet. Be the first to comment!