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.md
source 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

  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

{
  "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

  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.