Qaskills Jasmine Testing
BDD-style JavaScript testing with Jasmine covering spies, async patterns, custom matchers, clock manipulation, and comprehensive test organization for frontend and Node.js applications.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/jasmine-testing" ~/.claude/skills/pramoddutta-qaskills-jasmine-testing && rm -rf "$T"
manifest:
seed-skills/jasmine-testing/SKILL.mdsource content
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
, andit
in natural language.expect - One expectation focus per spec -- Each
block should verify a single logical behavior to make failures easy to diagnose.it - Arrange-Act-Assert -- Structure every spec into setup, execution, and verification phases even when using
.beforeEach - Isolate with spies -- Use
andjasmine.createSpy()
to eliminate external dependencies and side effects.jasmine.createSpyObj() - 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
blocks.afterEach - Prefer async/await -- Use modern async patterns over
callbacks for cleaner, more readable async specs.done()
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
for shared setup -- Avoid duplicating setup code across specs; put common initialization inbeforeEach
blocks for consistency and DRY code.beforeEach - Always uninstall Jasmine clock -- If you call
, always pair it withjasmine.clock().install()
injasmine.clock().uninstall()
to prevent cross-spec contamination.afterEach - Use
for partial matches -- When testing objects with dynamic fields like timestamps or IDs, match only the fields you care about.jasmine.objectContaining - Prefer
over manual mocks -- It creates a clean mock with typed spy methods and avoids accidentally calling real implementations.createSpyObj - Test error paths explicitly -- Every function that can throw or reject should have specs for each error scenario.
- Randomize spec execution order -- Set
in jasmine.json to catch specs that accidentally depend on execution order.random: true - Use
andfdescribe
only during debugging -- Never commit focused specs to version control; they skip other tests silently.fit - 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
blocks -- Create a hierarchy that mirrors the conditions and behaviors being tested.describe
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
block makes it impossible to identify which behavior failed.it - Shared mutable state between specs -- Storing test state in variables outside
causes order-dependent failures that are difficult to debug.beforeEach - Using
callback with async/await -- Mixing callback and promise patterns leads to confusing control flow and potential false positives.done() - Catching exceptions in specs -- Wrapping code in try/catch inside a spec swallows failures; use
ortoThrow()
matchers instead.toThrowError() - 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.