Skip to content
Back to skills

Unit Testing

ASecurity

Unit testing patterns with Vitest - mocking, fixtures, isolation, and fast feedback.

  • 43 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 8, 2026
developmenttypescriptgokubernetestestingapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add LiorCohen/sdd --skill unit-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Unit Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Unit Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/liorcohen-unit-testing/badge)](https://www.skillsdirectory.com/skills/liorcohen-unit-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: unit-testing
description: Unit testing patterns with Vitest - mocking, fixtures, isolation, and fast feedback.
user-invocable: false
---


# Unit Testing Skill

Fast, isolated tests that verify individual functions and modules. Unit tests run in the CI runner (not Kubernetes) for rapid feedback.

---

## Overview

| Aspect | Details |
|--------|---------|
| Location | `components/*/src/**/*.test.ts` |
| Framework | Vitest |
| Runs In | CI runner (fast feedback) |
| Written By | Implementors (alongside their code) |

---

## Test Structure

### File Naming

Place test files next to the code they test:

```text
src/model/use-cases/
├── create_user.ts
├── create_user.test.ts      # Unit test for create_user
├── update_user.ts
└── update_user.test.ts
```

### Basic Pattern

```typescript
// src/model/use-cases/create_user.test.ts
import { describe, it, expect } from 'vitest';
import { createUser } from './create_user';
import type { Dependencies } from '../dependencies';

/**
 * @spec changes/user-management/SPEC.md
 * @issue PROJ-123
 */
describe('createUser', () => {
  describe('AC1: Valid user creation', () => {
    it('creates user when email is unique', async () => {
      // Arrange (Given)
      const mockDeps = createMockDependencies();
      const args = { email: 'test@example.com', name: 'Test User' };

      // Act (When)
      const result = await createUser(mockDeps, args);

      // Assert (Then)
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.user.email).toBe('test@example.com');
      }
    });
  });
});
```

---

## Mocking Strategies

### Dependency Injection Mocks

For functions that receive dependencies as the first argument (CMDO pattern):

```typescript
import type { Dependencies } from '../dependencies';

const createMockDependencies = (overrides?: Partial<Dependencies>): Dependencies => ({
  findUserByEmail: async () => null,
  findUserById: async () => null,
  insertUser: async (data) => ({ id: 'mock-id', ...data, createdAt: new Date() }),
  updateUser: async (id, data) => ({ id, ...data, updatedAt: new Date() }),
  ...overrides,
});

// Usage in tests
it('returns error when email exists', async () => {
  const deps = createMockDependencies({
    findUserByEmail: async () => ({ id: '123', email: 'exists@test.com', name: 'Existing' }),
  });

  const result = await createUser(deps, { email: 'exists@test.com', name: 'New' });

  expect(result.success).toBe(false);
});
```

### Vitest Mock Functions

For tracking calls and controlling return values:

```typescript
import { describe, it, expect, vi } from 'vitest';

it('calls insertUser with correct data', async () => {
  const insertUser = vi.fn().mockResolvedValue({ id: '123', email: 'test@example.com' });
  const deps = createMockDependencies({ insertUser });

  await createUser(deps, { email: 'test@example.com', name: 'Test' });

  expect(insertUser).toHaveBeenCalledOnce();
  expect(insertUser).toHaveBeenCalledWith(
    expect.objectContaining({ email: 'test@example.com' })
  );
});
```

### Module Mocks

For mocking entire modules (use sparingly):

```typescript
import { vi } from 'vitest';

vi.mock('../services/email', () => ({
  sendEmail: vi.fn().mockResolvedValue({ sent: true }),
}));

import { sendEmail } from '../services/email';

it('sends welcome email after user creation', async () => {
  await createUserWithWelcomeEmail(deps, { email: 'new@test.com' });

  expect(sendEmail).toHaveBeenCalledWith(
    expect.objectContaining({ to: 'new@test.com', template: 'welcome' })
  );
});
```

---

## Fixtures and Test Data

### Factory Functions

Create reusable factories for test data:

```typescript
// src/__tests__/fixtures/user.ts
import type { User } from '../../types/generated';

export const createTestUser = (overrides?: Partial<User>): User => ({
  id: 'test-user-id',
  email: 'test@example.com',
  name: 'Test User',
  role: 'planner',
  clientId: 'test-client',
  createdAt: new Date('2026-01-01'),
  updatedAt: new Date('2026-01-01'),
  ...overrides,
});

// Usage
it('formats user display name', () => {
  const user = createTestUser({ name: 'John Doe' });
  expect(formatDisplayName(user)).toBe('John Doe');
});

it('uses email when name is empty', () => {
  const user = createTestUser({ name: '', email: 'john@example.com' });
  expect(formatDisplayName(user)).toBe('john');
});
```

### Shared Test Constants

```typescript
// src/__tests__/fixtures/constants.ts
export const TEST_USER_ID = 'test-user-123';
export const TEST_CLIENT_ID = 'test-client-456';
export const TEST_EMAIL = 'test@example.com';

export const VALID_CREDENTIALS = {
  email: TEST_EMAIL,
  password: 'ValidPass123!',
};

export const INVALID_CREDENTIALS = {
  email: TEST_EMAIL,
  password: 'wrong',
};
```

---

## Isolation Strategies

### Reset State Between Tests

```typescript
import { describe, it, beforeEach, afterEach, vi } from 'vitest';

describe('userService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // Tests run in isolation
});
```

### Avoid Shared Mutable State

```typescript
// BAD: Shared mutable state
let testUser = { id: '1', name: 'Test' };

it('test 1', () => {
  testUser.name = 'Modified'; // Affects other tests!
});

// GOOD: Create fresh data per test
it('test 1', () => {
  const testUser = createTestUser();
  // Modifications don't affect other tests
});
```

### Time Mocking

```typescript
import { vi, beforeEach, afterEach } from 'vitest';

describe('time-dependent tests', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-01-15T10:00:00Z'));
  });

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

  it('calculates expiration correctly', () => {
    const token = createToken({ expiresIn: '1h' });
    expect(token.expiresAt).toEqual(new Date('2026-01-15T11:00:00Z'));
  });
});
```

---

## Testing Async Code

### Promises

```typescript
it('handles async operations', async () => {
  const result = await createUser(deps, validArgs);
  expect(result.success).toBe(true);
});

it('handles rejected promises', async () => {
  const deps = createMockDependencies({
    insertUser: async () => { throw new Error('DB connection failed'); },
  });

  await expect(createUser(deps, validArgs)).rejects.toThrow('DB connection failed');
});
```

### Testing Error Scenarios

```typescript
it('returns error result for validation failure', async () => {
  const result = await createUser(deps, { email: 'invalid-email', name: '' });

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('invalid_email');
  }
});

it('handles database errors gracefully', async () => {
  const deps = createMockDependencies({
    insertUser: async () => { throw new Error('Connection timeout'); },
  });

  const result = await createUser(deps, validArgs);

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('database_error');
  }
});
```

---

## Testing Discriminated Unions

For functions returning discriminated union results:

```typescript
type CreateUserResult =
  | { readonly success: true; readonly user: User }
  | { readonly success: false; readonly error: 'email_exists' | 'invalid_email' };

it('returns success with user data', async () => {
  const result = await createUser(deps, validArgs);

  // Type narrowing via discriminant
  expect(result.success).toBe(true);
  if (result.success) {
    expect(result.user.email).toBe('test@example.com');
    expect(result.user.id).toBeDefined();
  }
});

it('returns specific error code', async () => {
  const deps = createMockDependencies({
    findUserByEmail: async () => existingUser,
  });

  const result = await createUser(deps, { email: existingUser.email, name: 'New' });

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('email_exists');
  }
});
```

---

## Coverage Guidelines

### What to Cover

- All public functions
- All code paths (success, error, edge cases)
- All acceptance criteria from specs
- Boundary conditions

### What NOT to Cover

- Private implementation details
- Framework code
- Generated types
- Simple getters/setters

### Coverage Targets

| Metric | Target |
|--------|--------|
| Line coverage | ≥80% |
| Branch coverage | ≥75% |
| Function coverage | ≥90% |

---

## Rules

- **One test file per source file** - `create_user.ts` → `create_user.test.ts`
- **Test behavior, not implementation** - Focus on inputs/outputs, not internal details
- **Each AC = at least one test** - Map tests to acceptance criteria
- **Reference spec and issue** - Use `@spec` and `@issue` JSDoc tags
- **Given/When/Then structure** - Clear Arrange/Act/Assert sections
- **Fast execution** - Unit tests should complete in milliseconds
- **No external dependencies** - Mock all I/O (DB, HTTP, filesystem)
- **Isolated tests** - Tests must not depend on each other
- **Descriptive names** - Test names should describe the scenario

---

## Summary Checklist

Before committing unit tests, verify:

- [ ] Test file is next to source file with `.test.ts` suffix
- [ ] `@spec` and `@issue` tags present in file header
- [ ] Each acceptance criterion has corresponding tests
- [ ] Dependencies are mocked (no real I/O)
- [ ] Tests follow Given/When/Then structure
- [ ] Tests are isolated (no shared mutable state)
- [ ] All async operations properly awaited
- [ ] Error scenarios tested
- [ ] Tests run fast (< 100ms per test)

---

## Input / Output

This skill defines no input parameters or structured output.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.