.claude/skills/testing-patterns/SKILL.md
Jest testing strategies, test organization, factory patterns for test data, mocking strategies for authentication and external services, real-time message processor testing, test-driven development workflow, unit vs integration testing, fake timer usage for time-dependent tests, and testing best practices for ree-board project
npx skillsauth add DW225/ree-board testing-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Activate this skill when:
File Structure:
lib/
├── actions/
│ └── post/
│ ├── createPost.ts
│ └── createPost.test.ts
├── realtime/
│ ├── messageProcessors.ts
│ └── __tests__/
│ └── messageProcessors.test.ts
└── utils/
├── md5.ts
└── md5.test.ts
Naming Convention:
<filename>.test.ts__tests__/Arrange, Act, Assert:
describe('createPost', () => {
it('should create a post with valid data', async () => {
// Arrange
const boardId = 'board-123';
const content = 'Test post';
const type = 'went_well';
// Act
const result = await createPost(boardId, content, type);
// Assert
expect(result).toBeDefined();
expect(result.content).toBe(content);
expect(result.type).toBe(type);
});
});
Pattern for Testing Server Actions:
// __tests__/createPost.test.ts
import { createPost } from '../createPost';
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server';
// Mock Kinde authentication
jest.mock('@kinde-oss/kinde-auth-nextjs/server');
describe('createPost', () => {
const mockUserId = 'user-123';
beforeEach(() => {
// Setup mock authenticated user
(getKindeServerSession as jest.Mock).mockReturnValue({
getUser: jest.fn().mockResolvedValue({
id: mockUserId,
email: '[email protected]'
})
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('creates post for authenticated user', async () => {
const post = await createPost('board-1', 'Content', 'went_well');
expect(post.userId).toBe(mockUserId);
});
it('throws error for unauthenticated user', async () => {
// Override mock for this test
(getKindeServerSession as jest.Mock).mockReturnValue({
getUser: jest.fn().mockResolvedValue(null)
});
await expect(
createPost('board-1', 'Content', 'went_well')
).rejects.toThrow('Unauthorized');
});
});
Create Reusable Test Data Generators:
// __tests__/factories/postFactory.ts
import { nanoid } from 'nanoid';
export const createMockPost = (overrides?: Partial<Post>): Post => ({
id: nanoid(),
boardId: 'board-123',
userId: 'user-123',
content: 'Test post content',
type: 'went_well',
voteCount: 0,
createdAt: new Date(),
...overrides
});
export const createMockPosts = (count: number): Post[] =>
Array.from({ length: count }, () => createMockPost());
// Usage in tests
it('filters posts by type', () => {
const posts = [
createMockPost({ type: 'went_well' }),
createMockPost({ type: 'to_improve' }),
createMockPost({ type: 'went_well' })
];
const filtered = filterPostsByType(posts, 'went_well');
expect(filtered).toHaveLength(2);
});
Pattern with Fake Timers:
// lib/realtime/__tests__/messageProcessors.test.ts
import { processPostUpdate } from '../messageProcessors';
describe('Message Processors', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers(); // ✅ Use fake timers for time-dependent tests
});
afterEach(() => {
jest.useRealTimers();
});
it('processes valid messages', () => {
const validMessage = {
type: 'post:update',
postId: 'post-123',
content: 'Updated content',
userId: 'user-123',
timestamp: Date.now()
};
expect(() => processPostUpdate(validMessage)).not.toThrow();
});
it('rejects stale messages', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
const staleMessage = {
type: 'post:update',
postId: 'post-123',
content: 'Old content',
userId: 'user-123',
timestamp: Date.now() - 35000 // 35 seconds ago
};
processPostUpdate(staleMessage);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Stale message'),
expect.any(Object)
);
consoleSpy.mockRestore();
});
it('validates message structure', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
const invalidMessage = {
type: 'post:update',
// Missing required fields
};
processPostUpdate(invalidMessage);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid message'),
expect.objectContaining({ details: expect.any(Array) })
);
consoleSpy.mockRestore();
});
});
Reset Signal State:
import { postsSignal, filterSignal } from '@/lib/signal/postSignals';
import { createMockPost } from './factories/postFactory';
describe('Post Signals', () => {
beforeEach(() => {
// ✅ Reset signals before each test
postsSignal.value = [];
filterSignal.value = 'all';
});
it('filters posts correctly', () => {
postsSignal.value = [
createMockPost({ type: 'went_well' }),
createMockPost({ type: 'to_improve' })
];
filterSignal.value = 'went_well';
expect(filteredPosts.value).toHaveLength(1);
expect(filteredPosts.value[0].type).toBe('went_well');
});
});
Ably Mock:
// Mock Ably
jest.mock('ably/react', () => ({
useChannel: jest.fn(() => ({
channel: {
publish: jest.fn(),
subscribe: jest.fn(),
unsubscribe: jest.fn()
}
})),
useConnectionStateListener: jest.fn()
}));
it('publishes message to channel', () => {
const { result } = renderHook(() => usePostUpdates());
result.current.publishUpdate('post-123', 'New content');
expect(mockPublish).toHaveBeenCalledWith(
'post:update',
expect.objectContaining({
postId: 'post-123',
content: 'New content'
})
);
});
Database Mock:
// Mock Drizzle
jest.mock('@/db', () => ({
db: {
query: {
postTable: {
findFirst: jest.fn(),
findMany: jest.fn()
}
},
insert: jest.fn(() => ({
values: jest.fn(() => ({
returning: jest.fn()
}))
}))
}
}));
Pattern with Promises:
it('handles async server action', async () => {
const result = await createPost('board-1', 'Content', 'went_well');
expect(result).toBeDefined();
expect(result.id).toBeTruthy();
});
it('handles async errors', async () => {
// Mock failure
jest.spyOn(db, 'insert').mockRejectedValue(new Error('DB Error'));
await expect(
createPost('board-1', 'Content', 'went_well')
).rejects.toThrow('DB Error');
});
Bad:
describe('Tests', () => {
it('test 1', () => {
jest.spyOn(console, 'log').mockImplementation();
// ❌ Never restored
});
it('test 2', () => {
// console.log still mocked!
});
});
Good:
describe('Tests', () => {
it('test 1', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
// test code
consoleSpy.mockRestore(); // ✅ Cleaned up
});
// OR use afterEach
afterEach(() => {
jest.restoreAllMocks();
});
});
Bad:
it('uses Array.filter internally', () => {
const filterSpy = jest.spyOn(Array.prototype, 'filter');
filterPosts(posts, 'went_well');
expect(filterSpy).toHaveBeenCalled(); // ❌ Tests implementation
});
Good:
it('returns only went_well posts', () => {
const filtered = filterPosts(posts, 'went_well');
expect(filtered.every(p => p.type === 'went_well')).toBe(true); // ✅ Tests behavior
});
Bad:
it('filters stale messages', async () => {
const oldMessage = { timestamp: Date.now() - 35000 };
// ❌ Test depends on real time passage
await new Promise(resolve => setTimeout(resolve, 100));
expect(isStale(oldMessage)).toBe(true);
});
Good:
it('filters stale messages', () => {
jest.useFakeTimers();
const now = Date.now();
const oldMessage = { timestamp: now - 35000 };
jest.setSystemTime(now);
expect(isStale(oldMessage)).toBe(true); // ✅ Deterministic
jest.useRealTimers();
});
Bad:
let sharedState = []; // ❌ Shared between tests
it('test 1', () => {
sharedState.push(1);
expect(sharedState).toHaveLength(1);
});
it('test 2', () => {
// Fails because sharedState has item from test 1
expect(sharedState).toHaveLength(0);
});
Good:
describe('Tests', () => {
let testState: number[];
beforeEach(() => {
testState = []; // ✅ Reset before each test
});
it('test 1', () => {
testState.push(1);
expect(testState).toHaveLength(1);
});
it('test 2', () => {
expect(testState).toHaveLength(0); // ✅ Passes
});
});
jest.config.js - Jest configurationlib/realtime/__tests__/ - Message processor testslib/utils/md5.test.ts - Example utility test// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1'
}
};
# Run all tests
pnpm test
# Run specific test file
pnpm test lib/utils/md5.test.ts
# Run in watch mode
pnpm test --watch
# Run with coverage
pnpm test --coverage
Message Processor Test:
describe('processPostUpdate', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();
});
afterEach(() => {
jest.useRealTimers();
});
it('processes valid message', () => {
const message = createValidMessage();
expect(() => processPostUpdate(message)).not.toThrow();
});
});
Server Action Test:
describe('createPost', () => {
beforeEach(() => {
mockKindeAuth();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('creates post', async () => {
const post = await createPost(boardId, content, type);
expect(post).toBeDefined();
});
});
Last Updated: 2026-01-10
development
Preact Signals for reactive state management, signal vs computed signal usage, batch updates for performance, action creator patterns, signal integration with React components, state management by domain (boards posts members), reactive patterns, and signal best practices for ree-board project
testing
Role-based access control (RBAC) patterns, authentication wrappers, authorization checks, input validation with Zod schemas, security boundaries, server action security, real-time message validation, preventing common vulnerabilities like XSS and SQL injection, and security best practices for ree-board project
tools
Next.js 16 App Router patterns including server components, client components, server actions, route handlers, layouts, metadata API, dynamic routes, file conventions, data fetching, caching strategies, and Next.js best practices for building modern React applications
data-ai
Drizzle ORM best practices including schema design with relationships, database migrations, prepared statements for performance, transactions, indexes, Turso SQLite database operations, type safety patterns, query optimization, and database workflow for ree-board project