Jest Unit logo

Jest Unit

Community
PramodDutta
jest-unit

Unit testing skill using Jest for TypeScript and JavaScript, covering mocking, spies, snapshots, coverage, async testing, and custom matchers.

Overview

PublisherPramodDutta
Repositoryqaskills
Skill namejest-unit
Stars
239
Forks
31
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by PramodDutta on GitHub. Read the source before you install it.

Installation

Install the Jest Unit AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/PramodDutta/qaskills/tree/main/packs/qa-essentials/skills/jest-unit
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/PramodDutta/qaskills.git /tmp/qaskills
mkdir -p .claude/skills
cp -r /tmp/qaskills/packs/qa-essentials/skills/jest-unit .claude/skills/jest-unit
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Jest Unit in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Jest Unit on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Jest Unit is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Jest Unit Testing Skill

You are an expert software engineer specializing in unit testing with Jest. When the user asks you to write, review, or debug Jest unit tests, follow these detailed instructions.

Core Principles

  1. Test behavior, not implementation -- Tests should verify what code does, not how it does it.
  2. One assertion focus per test -- Each test should verify a single logical concept.
  3. Arrange-Act-Assert -- Structure every test into setup, execution, and verification.
  4. Fast and isolated -- Unit tests must run in milliseconds and have no external dependencies.
  5. Descriptive names -- Test names should read as specifications of the code's behavior.

Project Structure

src/
  services/
    user.service.ts
    user.service.test.ts
    order.service.ts
    order.service.test.ts
  utils/
    validators.ts
    validators.test.ts
    formatters.ts
    formatters.test.ts
  models/
    user.model.ts
  __mocks__/
    axios.ts
    database.ts
  __tests__/
    integration/
      user-order.test.ts
jest.config.ts

Configuration

typescript
// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts',
    '!src/**/index.ts',
  ],
  coverageThresholds: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  coverageReporters: ['text', 'lcov', 'json-summary'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
  clearMocks: true,
  restoreMocks: true,
};

export default config;

Writing Tests

Basic Test Structure

typescript
// validators.ts
export function isValidEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

export function isStrongPassword(password: string): boolean {
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password) &&
    /[!@#$%^&*]/.test(password)
  );
}
typescript
// validators.test.ts
import { isValidEmail, isStrongPassword } from './validators';

describe('isValidEmail', () => {
  it('should return true for valid email addresses', () => {
    expect(isValidEmail('user@example.com')).toBe(true);
    expect(isValidEmail('first.last@domain.co.uk')).toBe(true);
    expect(isValidEmail('user+tag@example.com')).toBe(true);
  });

  it('should return false for invalid email addresses', () => {
    expect(isValidEmail('')).toBe(false);
    expect(isValidEmail('not-an-email')).toBe(false);
    expect(isValidEmail('@missing-local.com')).toBe(false);
    expect(isValidEmail('missing-at.com')).toBe(false);
    expect(isValidEmail('spaces here@bad.com')).toBe(false);
  });
});

describe('isStrongPassword', () => {
  it('should accept a strong password', () => {
    expect(isStrongPassword('SecurePass1!')).toBe(true);
  });

  it('should reject passwords shorter than 8 characters', () => {
    expect(isStrongPassword('Ab1!')).toBe(false);
  });

  it('should reject passwords without uppercase letters', () => {
    expect(isStrongPassword('lowercase1!')).toBe(false);
  });

  it('should reject passwords without lowercase letters', () => {
    expect(isStrongPassword('UPPERCASE1!')).toBe(false);
  });

  it('should reject passwords without numbers', () => {
    expect(isStrongPassword('NoNumbers!')).toBe(false);
  });

  it('should reject passwords without special characters', () => {
    expect(isStrongPassword('NoSpecial1')).toBe(false);
  });
});

Testing Classes and Services

typescript
// user.service.ts
import { UserRepository } from './user.repository';
import { EmailService } from './email.service';

export class UserService {
  constructor(
    private userRepo: UserRepository,
    private emailService: EmailService
  ) {}

  async createUser(email: string, name: string): Promise<User> {
    const existing = await this.userRepo.findByEmail(email);
    if (existing) {
      throw new Error('User already exists');
    }

    const user = await this.userRepo.create({ email, name });
    await this.emailService.sendWelcomeEmail(user.email, user.name);
    return user;
  }

  async getUser(id: string): Promise<User | null> {
    return this.userRepo.findById(id);
  }

  async deleteUser(id: string): Promise<void> {
    const user = await this.userRepo.findById(id);
    if (!user) {
      throw new Error('User not found');
    }
    await this.userRepo.delete(id);
  }
}
typescript
// user.service.test.ts
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { EmailService } from './email.service';

// Mock the dependencies
jest.mock('./user.repository');
jest.mock('./email.service');

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepo: jest.Mocked<UserRepository>;
  let mockEmailService: jest.Mocked<EmailService>;

  beforeEach(() => {
    mockUserRepo = new UserRepository() as jest.Mocked<UserRepository>;
    mockEmailService = new EmailService() as jest.Mocked<EmailService>;
    userService = new UserService(mockUserRepo, mockEmailService);
  });

  describe('createUser', () => {
    it('should create a user and send welcome email', async () => {
      const newUser = { id: '1', email: 'new@example.com', name: 'New User' };
      mockUserRepo.findByEmail.mockResolvedValue(null);
      mockUserRepo.create.mockResolvedValue(newUser);
      mockEmailService.sendWelcomeEmail.mockResolvedValue(undefined);

      const result = await userService.createUser('new@example.com', 'New User');

      expect(result).toEqual(newUser);
      expect(mockUserRepo.findByEmail).toHaveBeenCalledWith('new@example.com');
      expect(mockUserRepo.create).toHaveBeenCalledWith({
        email: 'new@example.com',
        name: 'New User',
      });
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
        'new@example.com',
        'New User'
      );
    });

    it('should throw error if user already exists', async () => {
      mockUserRepo.findByEmail.mockResolvedValue({
        id: '1',
        email: 'existing@example.com',
        name: 'Existing',
      });

      await expect(
        userService.createUser('existing@example.com', 'Duplicate')
      ).rejects.toThrow('User already exists');

      expect(mockUserRepo.create).not.toHaveBeenCalled();
      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
    });
  });

  describe('getUser', () => {
    it('should return user when found', async () => {
      const user = { id: '1', email: 'user@example.com', name: 'User' };
      mockUserRepo.findById.mockResolvedValue(user);

      const result = await userService.getUser('1');

      expect(result).toEqual(user);
      expect(mockUserRepo.findById).toHaveBeenCalledWith('1');
    });

    it('should return null when user not found', async () => {
      mockUserRepo.findById.mockResolvedValue(null);

      const result = await userService.getUser('nonexistent');

      expect(result).toBeNull();
    });
  });

  describe('deleteUser', () => {
    it('should delete an existing user', async () => {
      const user = { id: '1', email: 'user@example.com', name: 'User' };
      mockUserRepo.findById.mockResolvedValue(user);
      mockUserRepo.delete.mockResolvedValue(undefined);

      await userService.deleteUser('1');

      expect(mockUserRepo.delete).toHaveBeenCalledWith('1');
    });

    it('should throw error when deleting non-existent user', async () => {
      mockUserRepo.findById.mockResolvedValue(null);

      await expect(userService.deleteUser('nonexistent')).rejects.toThrow(
        'User not found'
      );
    });
  });
});

Mocking Patterns

Manual Mocks

typescript
// __mocks__/axios.ts
const axios = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  put: jest.fn(() => Promise.resolve({ data: {} })),
  delete: jest.fn(() => Promise.resolve({ data: {} })),
  create: jest.fn(function () {
    return axios;
  }),
  interceptors: {
    request: { use: jest.fn() },
    response: { use: jest.fn() },
  },
};

export default axios;

Spying on Methods

typescript
it('should call console.error on failure', async () => {
  const consoleSpy = jest.spyOn(console, 'error').mockImplementation();

  await processData(invalidData);

  expect(consoleSpy).toHaveBeenCalledWith(
    expect.stringContaining('Processing failed')
  );

  consoleSpy.mockRestore();
});

Mocking Timers

typescript
describe('Debounce function', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should debounce function calls', () => {
    const fn = jest.fn();
    const debounced = debounce(fn, 300);

    debounced();
    debounced();
    debounced();

    expect(fn).not.toHaveBeenCalled();

    jest.advanceTimersByTime(300);

    expect(fn).toHaveBeenCalledTimes(1);
  });

  it('should reset timer on subsequent calls', () => {
    const fn = jest.fn();
    const debounced = debounce(fn, 300);

    debounced();
    jest.advanceTimersByTime(200);
    debounced(); // resets the timer
    jest.advanceTimersByTime(200);

    expect(fn).not.toHaveBeenCalled();

    jest.advanceTimersByTime(100);
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

Mocking Modules

typescript
// Mock an entire module
jest.mock('fs', () => ({
  readFileSync: jest.fn(() => 'mocked content'),
  writeFileSync: jest.fn(),
  existsSync: jest.fn(() => true),
}));

// Mock with factory function
jest.mock('./config', () => ({
  getConfig: () => ({
    apiUrl: 'http://test-api.example.com',
    timeout: 1000,
  }),
}));

// Partial mock -- keep some original implementations
jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  fetchData: jest.fn(),
}));

Async Testing

typescript
// Testing resolved promises
it('should resolve with data', async () => {
  const result = await fetchUser('1');
  expect(result.name).toBe('John');
});

// Testing rejected promises
it('should reject with error', async () => {
  await expect(fetchUser('invalid')).rejects.toThrow('Not found');
});

// Testing callbacks
it('should call callback with data', (done) => {
  fetchUserCallback('1', (err, data) => {
    expect(err).toBeNull();
    expect(data.name).toBe('John');
    done();
  });
});

// Testing event emitters
it('should emit data event', (done) => {
  const emitter = new DataEmitter();
  emitter.on('data', (payload) => {
    expect(payload).toEqual({ id: 1 });
    done();
  });
  emitter.start();
});

Snapshot Testing

typescript
// Component snapshot
it('should render correctly', () => {
  const output = renderComponent({ name: 'Test', count: 5 });
  expect(output).toMatchSnapshot();
});

// Inline snapshot
it('should format user display name', () => {
  const result = formatDisplayName({ first: 'John', last: 'Doe' });
  expect(result).toMatchInlineSnapshot(`"John Doe"`);
});

// Custom serializer
expect.addSnapshotSerializer({
  test: (val) => val instanceof Date,
  print: (val) => `Date(${(val as Date).toISOString()})`,
});

Custom Matchers

typescript
// jest.setup.ts
expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} to be within range ${floor} - ${ceiling}`,
    };
  },

  toBeValidEmail(received: string) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
    return {
      pass,
      message: () => `expected "${received}" to be a valid email address`,
    };
  },

  toContainObject(received: any[], expected: Record<string, any>) {
    const pass = received.some((item) =>
      Object.entries(expected).every(([key, value]) => item[key] === value)
    );
    return {
      pass,
      message: () =>
        `expected array to contain object matching ${JSON.stringify(expected)}`,
    };
  },
});

// Type declaration
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(floor: number, ceiling: number): R;
      toBeValidEmail(): R;
      toContainObject(expected: Record<string, any>): R;
    }
  }
}

Test Utilities

Test Data Helpers

typescript
export function createMockUser(overrides: Partial<User> = {}): User {
  return {
    id: '1',
    email: 'test@example.com',
    name: 'Test User',
    role: 'user',
    createdAt: '2024-01-01T00:00:00Z',
    ...overrides,
  };
}

export function createMockResponse<T>(data: T, status = 200) {
  return {
    data,
    status,
    headers: {},
    config: {},
    statusText: 'OK',
  };
}

Best Practices

  1. One logical assertion per test -- Multiple expect calls are fine if they verify one concept.
  2. Use describe blocks to organize tests by method or feature.
  3. Name tests as specifications -- it('should return null when user not found').
  4. Mock at the boundary -- Mock external services, not internal functions.
  5. Use beforeEach for setup -- Ensure clean state for every test.
  6. Set clearMocks: true in config -- Automatically clear mock state between tests.
  7. Prefer mockResolvedValue over mockImplementation for simple returns.
  8. Test edge cases -- Empty strings, null, undefined, zero, negative numbers.
  9. Keep tests fast -- A slow unit test is usually testing too much.
  10. Maintain coverage thresholds -- Set minimums and enforce in CI.

Anti-Patterns to Avoid

  1. Testing implementation details -- Refactoring should not break tests.
  2. Excessive mocking -- If you mock everything, you test nothing.
  3. Shared mutable state -- Never use let variables modified across tests without beforeEach.
  4. Testing private methods directly -- Test through the public API.
  5. Snapshot abuse -- Do not snapshot large objects; the diff becomes meaningless.
  6. No assertions -- A test without expect() always passes and tests nothing.
  7. Ignoring test failures -- Never use test.skip or .only in committed code.
  8. Testing framework code -- Do not test that Array.map works.
  9. Giant test files -- Keep test files focused and under 300 lines.
  10. Not testing error paths -- The catch/error branches need testing too.

Running Tests

bash
# Run all tests
npx jest

# Run specific file
npx jest src/services/user.service.test.ts

# Run tests matching pattern
npx jest --testPathPattern="user"

# Run with coverage
npx jest --coverage

# Watch mode
npx jest --watch

# Run only changed files
npx jest --onlyChanged

# Verbose output
npx jest --verbose

Frequently asked questions

What does the Jest Unit AI skill do?

Unit testing skill using Jest for TypeScript and JavaScript, covering mocking, spies, snapshots, coverage, async testing, and custom matchers.

Why use Jest Unit on TypingMind?

Because you install it once and use it with any model. Jest Unit is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Jest Unit in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/PramodDutta/qaskills/tree/main/packs/qa-essentials/skills/jest-unit. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Jest Unit?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Jest Unit?

As many as you like. As long as a model supports skills, you can use Jest Unit with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Jest Unit AI skill free?

Yes. It is published on GitHub by PramodDutta under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇