# 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.

- Skill: `pramoddutta/jasmine-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pramoddutta/jasmine-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pramoddutta/jasmine-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: PramodDutta (https://skillmd.com/u/pramoddutta)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/pramoddutta/jasmine-testing

---


# 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.

