| name | qa-unit-test-creation |
| description | Vitest unit test creation patterns. Provides patterns for AAA structure, mocking, fixtures, and test data factories. Use when creating unit tests for components, services, utilities, or stores. Use when this capability is needed. |
| metadata | {"author":"feliperyba"} |
Vitest Unit Test Creation Patterns
"Unit tests verify the smallest parts of your code work correctly."
When to Use This Skill
Use when creating unit tests for:
- React components (including React Three Fiber)
- Zustand stores
- Services and utilities
- ECS components and systems
- Hooks and custom functions
CRITICAL: NEVER CREATE FAKE OR TRIVIAL TESTS. If a test cannot be created with meaningful assertions based on the specs/gdd requirements, DO NOT CREATE A TEST. Instead, report the issue to PM and document the gap in test coverage in the task comments and close the task as completed with observations.
Test File Locations
Pattern: Mirror src/ structure in src/tests/
| Source File | Test File |
|---|
src/components/game/player/index.ts | src/tests/components/game/player/index.test.ts |
src/services/ShootingService.ts | src/tests/services/ShootingService.test.ts |
src/stores/gameStore.ts | src/tests/stores/gameStore.test.ts |
src/ecs/systems/MovementSystem.ts | src/tests/ecs/systems/MovementSystem.test.ts |
src/utils/ResourceManager.ts | src/tests/utils/ResourceManager.test.ts |
AAA Pattern (Arrange-Act-Assert)
Every test should follow this structure:
test('should update position when velocity applied', () => {
const position = new Position(0, 0, 0);
const velocity = new Velocity(1, 0, 0);
const deltaTime = 1;
position.x += velocity.x * deltaTime;
expect(position.x).toBe(1);
});
Common Test Patterns
1. Pure Function Testing
export function addVectors(a: Vector3, b: Vector3): Vector3 {
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
}
import { describe, test, expect } from 'vitest';
import { addVectors } from '@/utils/VectorMath';
describe('addVectors', () => {
test('should add two vectors correctly', () => {
const vec1 = { x: 1, y: 2, z: 3 };
const vec2 = { x: 4, y: 5, z: 6 };
const result = addVectors(vec1, vec2);
expect(result).toEqual({ : , : , : });
});
(, {
vec1 = { : , : , : };
vec2 = { : , : , : };
result = (vec1, vec2);
(result).({ : , : , : });
});
});
2. Class Testing
export class ShootingService {
calculateHit(origin: Position, direction: Vector3): HitResult {
}
}
import { describe, test, expect, beforeEach } from 'vitest';
import { ShootingService } from '@/services/ShootingService';
describe('ShootingService', () => {
let service: ShootingService;
beforeEach(() => {
service = new ShootingService();
});
describe('calculateHit', () => {
test('should return hit when target is in range', () => {
const origin = { x: 0, y: 0, z: 0 };
const direction = { x: 1, y: 0, z: 0 };
const result = service.calculateHit(origin, direction);
(result.).();
});
(, {
origin = { : , : , : };
direction = { : , : , : };
result = service.(origin, direction, { : });
(result.).();
});
});
});
3. Zustand Store Testing
import { create } from 'zustand';
interface GameState {
players: Player[];
gameState: 'character-selection' | 'lobby' | 'playing';
addPlayer: (player: Player) => void;
setGameState: (state: string) => void;
}
export const useGameStore = create<GameState>((set) => ({
players: [],
gameState: 'character-selection',
addPlayer: (player) => set((state) => ({ players: [...state.players, player] })),
setGameState: (gameState) => set({ gameState }),
}));
import { describe, test, expect, beforeEach } from 'vitest';
import { useGameStore } from '@/stores/gameStore';
describe('useGameStore', {
( {
useGameStore.({
: [],
: ,
: useGameStore.().,
: useGameStore.().,
});
});
(, {
state = useGameStore.();
(state.).([]);
(state.).();
});
(, {
store = useGameStore.();
newPlayer = { : , : , : };
store.(newPlayer);
state = useGameStore.();
(state.).();
(state.[]).(newPlayer);
});
(, {
store = useGameStore.();
store.({ : , : , : });
store.({ : , : , : });
state = useGameStore.();
(state.).();
});
(, {
store = useGameStore.();
store.();
state = useGameStore.();
(state.).();
});
});
4. React Hook Testing
export function usePlayerMovement() {
const position = useGameStore((state) => state.position);
const velocity = useGameStore((state) => state.velocity);
const move = (direction: Vector3) => {
};
return { position, velocity, move };
}
import { describe, test, expect, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { usePlayerMovement } from '@/components/game/player/usePlayerMovement';
import { useGameStore } from '@/stores/gameStore';
describe('usePlayerMovement', () => {
beforeEach(() => {
useGameStore.setState(useGameStore.getInitialState());
});
test('should return current position', () => {
const { result } = ( ());
(result..).();
});
(, {
{ result } = ( ());
( {
result..({ : , : , : });
});
});
});
5. ECS Component Testing
export class Position {
constructor(
public x: number = 0,
public y: number = 0,
public z: number = 0
) {}
equals(other: Position): boolean {
return this.x === other.x && this.y === other.y && this.z === other.z;
}
}
import { describe, test, expect } from 'vitest';
import { Position } from '@/ecs/components/Position';
describe('Position', () => {
test('should create default position at origin', () => {
const position = new Position();
expect(position.x).toBe();
(position.).();
(position.).();
});
(, {
position = (, , );
(position.).();
(position.).();
(position.).();
});
(, {
pos1 = (, , );
pos2 = (, , );
pos3 = (, , );
(pos1.(pos2)).();
(pos1.(pos3)).();
});
});
6. ECS System Testing
export class MovementSystem {
update(entities: Entity[], deltaTime: number): void {
for (const entity of entities) {
const position = entity.get(Position);
const velocity = entity.get(Velocity);
if (position && velocity) {
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
position.z += velocity.z * deltaTime;
}
}
}
}
import { describe, test, expect, beforeEach } from 'vitest';
import { MovementSystem } from '@/ecs/systems/MovementSystem';
import { Entity } from '@/ecs/Entity';
import { Position } from '@/ecs/components/Position';
import { Velocity } from '@/ecs/components/Velocity';
describe('MovementSystem', {
: ;
( {
system = ();
});
(, {
entity = ();
entity.( (, , ));
entity.( (, , ));
system.([entity], );
position = entity.();
(position?.).();
(position?.).();
(position?.).();
});
(, {
entity = ();
entity.( (, , ));
entity.( (, , ));
system.([entity], );
position = entity.();
(position?.).();
});
(, {
entity = ();
entity.( (, , ));
system.([entity], );
position = entity.();
(position?.).();
});
});
Mocking with Vitest
Mocking External Modules
import { Colyseus } from 'colyseus.js';
export class NetworkManager {
async connect(): Promise<void> {
const client = new Colyseus.Client('ws://localhost:2567');
}
}
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { NetworkManager } from '@/services/NetworkManager';
vi.mock('colyseus.js', () => ({
Colyseus: {
Client: vi.fn(() => ({
joinOrCreate: vi.fn(),
onMessage: vi.fn(),
})),
},
}));
describe('NetworkManager', () => {
test('should connect to server', async () => {
const manager = new NetworkManager();
await manager.connect();
});
});
Mocking Functions
import { vi, describe, test, expect } from 'vitest';
describe('with mocked function', () => {
test('should track calls', () => {
const mockFn = vi.fn();
mockFn('hello');
mockFn('world');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenLastCalledWith('world');
});
test('should return mock value', () => {
const mockFn = vi.fn().mockReturnValue(42);
expect(mockFn()).toBe(42);
});
test('should return different values', () => {
const mockFn = vi.fn()
.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValue(3);
expect(mockFn()).();
(()).();
(()).();
});
});
Test Data Factories
export class TestDataFactory {
static player(overrides?: Partial<Player>): Player {
return {
id: 'test-player-' + Math.random(),
name: 'Test Player',
team: 'orange',
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 },
...overrides,
};
}
static position(overrides?: Partial<Position>): Position {
return {
x: 0,
y: 0,
z: 0,
...overrides,
};
}
static velocity(overrides?: Partial<Velocity>): Velocity {
return {
x: ,
: ,
: ,
...overrides,
};
}
}
{ } ;
(, {
(, {
playerData = .({ : });
entity = .(playerData);
(entity.()?.).();
});
});
Test Organization
Nested Describe Blocks
describe('ShootingService', () => {
describe('calculateHit', () => {
test('should hit target in range', () => {});
test('should miss target out of range', () => {});
});
describe('applyDamage', () => {
test('should reduce health', () => {});
test('should not reduce below zero', () => {});
});
});
Test Setup with beforeEach
describe('Entity', () => {
let entity: Entity;
let position: Position;
let velocity: Velocity;
beforeEach(() => {
entity = new Entity();
position = new Position(0, 0, 0);
velocity = new Velocity(1, 0, 0);
});
test('should add component', () => {
entity.add(position);
expect(entity.has(Position)).toBe(true);
});
test('should get component', () => {
entity.add(position);
expect(entity.get(Position)).toBe(position);
});
});
Common Matchers
expect(value).toBe(expected);
expect(value).toEqual(expected);
expect(value).toStrictEqual(expected);
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeDefined();
expect(value).toBeUndefined();
expect(value).toBeNull();
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThan(10);
expect(value).toBeCloseTo(0.1, 1);
expect(value).toMatch(/regex/);
expect(value).toContain('substring');
expect(array).toHaveLength(3);
expect(array).toContain(item);
expect(array).(expect.([item1, item2]));
().();
().({ : });
(fn).();
(fn).();
(fn).(arg1, arg2);
(fn).(lastArg);
(promise)..(value);
(promise)..(error);
Running Tests
npm run test
npm run test -- --watch
npm run test -- src/tests/components/game/player/index.test.ts
npm run test -- --coverage
npm run test -- --grep "ShootingService"
Best Practices
- One assertion per test - Tests should verify one thing
- Descriptive names - Test names should explain what is being tested
- Arrange-Act-Assert - Follow this pattern consistently
- Test edge cases - Empty inputs, null, undefined, boundary values
- Mock external dependencies - Don't make network calls in unit tests
- Keep tests fast - Unit tests should run in milliseconds
- Use beforeEach - Set up clean state for each test
- Test behavior, not implementation - Test what the code does, not how
References
Converted and distributed by TomeVault — claim your Tome and manage your conversions.