| 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. |
| category | workflow |
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