Testing Patterns
This document outlines patterns for testing components of the ReX content system. Effective testing ensures system reliability, helps prevent regressions, and validates that components work as expected across different environments.
Unit Testing Pattern
Pattern Overview
The Unit Testing pattern validates individual components in isolation from their dependencies.
Implementation Example
import { describe, test, expect, vi } from 'vitest'
import { createMemoryAdapter } from '@lib/content/adapters/common/memory'
import { ContentNotFoundError } from '@lib/errors/content'
// Unit tests for memory adapter
describe('MemoryAdapter', () => {
// Reset adapter between tests
let adapter
beforeEach(() => {
adapter = createMemoryAdapter()
})
// Test read operation
test('read returns content when it exists', async () => {
// Setup: Add content to adapter
const content = {
data: 'Test content',
contentType: 'text/plain',
metadata: { title: 'Test' },
}
await adapter.write('test.md', content)
// Test: Read content
const result = await adapter.read('test.md')
// Assert: Content matches what was written
expect(result).toEqual(content)
})
// Test error case
test('read throws ContentNotFoundError for missing content', async () => {
// Test: Try to read non-existent content
await expect(adapter.write('test')).rejects.toThrow(ContentNotFoundError)
})
// Test write and delete operations
test('write and delete operations update storage correctly', async () => {
// Setup: Initial content
const content = {
data: 'Test content',
contentType: 'text/plain',
metadata: { title: 'Test' },
}
// Test: Write operation
await adapter.write('test.md', content)
// Assert: Content exists after write
const afterWrite = await adapter.read('test.md')
expect(afterWrite).toEqual(content)
// Test: Delete operation
await adapter.delete('test.md')
// Assert: Content doesn't exist after delete
await expect(adapter.read('test.md')).rejects.toThrow(ContentNotFoundError)
})
// Test events
test('events are emitted for write and delete operations', async () => {
// Setup: Event listeners
const writeListener = vi.fn()
const deleteListener = vi.fn()
adapter.events.on('change', event => {
if (event.type === 'write') {
writeListener(event)
} else if (event.type === 'delete') {
deleteListener(event)
}
})
// Test: Write operation
const content = {
data: 'Test content',
contentType: 'text/plain',
metadata: { title: 'Test' },
}
await adapter.write('test.md', content)
// Assert: Write event was emitted
expect(writeListener).toHaveBeenCalledWith(
expect.objectContaining({
uri: 'test.md',
content,
type: 'write',
})
)
// Test: Delete operation
await adapter.delete('test.md')
// Assert: Delete event was emitted
expect(deleteListener).toHaveBeenCalledWith(
expect.objectContaining({
uri: 'test.md',
content: null,
type: 'delete',
})
)
})
// Test list operation
test('list returns content URIs matching pattern', async () => {
// Setup: Add content
await adapter.write('docs/a.md', {
data: 'A',
contentType: 'text/markdown',
})
await adapter.write('docs/b.md', {
data: 'B',
contentType: 'text/markdown',
})
await adapter.write('docs/sub/c.md', {
data: 'C',
contentType: 'text/markdown',
})
await adapter.write('other/d.md', {
data: 'D',
contentType: 'text/markdown',
})
// Test: List with patterns
const allDocs = await adapter.list('docs/**')
const rootDocs = await adapter.list('docs/*.md')
const all = await adapter.list('**')
// Assert: Correct URIs returned
expect(allDocs).toHaveLength(3)
expect(allDocs).toContain('docs/a.md')
expect(allDocs).toContain('docs/b.md')
expect(allDocs).toContain('docs/sub/c.md')
expect(rootDocs).toHaveLength(2)
expect(rootDocs).toContain('docs/a.md')
expect(rootDocs).toContain('docs/b.md')
expect(all).toHaveLength(4)
})
})
Considerations
- Test each component in isolation with dependencies mocked
- Focus on the component’s public API rather than implementation details
- Include both success and error paths
- Use setup and teardown to ensure tests are independent
Related Patterns
- Dependency Injection Pattern - Providing testable dependencies
- Test Double Pattern - Creating mock implementations for testing
Integration Testing Pattern
Pattern Overview
The Integration Testing pattern tests how components work together in realistic configurations.
Implementation Example
import { describe, test, expect, vi } from 'vitest'
import { createContentStore } from '@lib/content/store/factory'
import { createMemoryAdapter } from '@lib/content/adapters/common/memory'
import { withValidation } from '@lib/content/middleware/validation'
import { withLogging } from '@lib/content/middleware/logging'
import { ContentValidationError } from '@lib/errors/content'
// Integration tests for content store with middleware
describe('ContentStore with middleware', () => {
// Create test schema
const testSchema = {
validate: content => {
// Simple validation: must have data and contentType
if (!content.data) {
return { valid: false, errors: ['Missing content data'] }
}
if (!content.contentType) {
return { valid: false, errors: ['Missing content type'] }
}
return { valid: true }
},
}
// Mock logger
const mockLogger = {
log: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
}
// Setup test store
let store
beforeEach(() => {
// Reset mocks
vi.resetAllMocks()
// Create adapter
const adapter = createMemoryAdapter()
// Create enhanced store with middleware
store = createContentStore({
adapter,
enhancers: [
withValidation(testSchema),
withLogging({ logger: mockLogger }),
],
})
})
// Test valid content write with middleware chain
test('valid content passes through middleware chain', async () => {
// Setup: Valid content
const content = {
data: 'Test content',
contentType: 'text/plain',
metadata: { title: 'Test' },
}
// Test: Write content
await store.write('test.md', content)
// Assert: Content was written
const result = await store.read('test.md')
expect(result).toEqual(content)
// Assert: Logger was called
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('write'),
expect.objectContaining({
uri: 'test.md',
operation: 'write',
})
)
})
// Test validation error
test('invalid content is rejected by validation middleware', async () => {
// Setup: Invalid content (missing contentType)
const invalidContent = {
data: 'Test content',
metadata: { title: 'Test' },
}
// Test: Try to write invalid content
await expect(store.write('test.md', invalidContent)).rejects.toThrow(
ContentValidationError
)
// Assert: Error was logged
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('validation'),
expect.objectContaining({
uri: 'test.md',
operation: 'write',
error: expect.any(ContentValidationError),
})
)
})
// Test middleware order
test('middleware executes in correct order', async () => {
// Setup: Capture order of execution
const executionOrder = []
// Create tracking middleware
const trackingMiddleware1 = next => async params => {
executionOrder.push('middleware1:before')
const result = await next(params)
executionOrder.push('middleware1:after')
return result
}
const trackingMiddleware2 = next => async params => {
executionOrder.push('middleware2:before')
const result = await next(params)
executionOrder.push('middleware2:after')
return result
}
// Create store with tracking middleware
const orderedStore = createContentStore({
adapter: createMemoryAdapter(),
enhancers: [trackingMiddleware1, trackingMiddleware2],
})
// Test: Execute operation
await orderedStore.read('test.md').catch(() => {})
// Assert: Middleware executed in correct order
expect(executionOrder).toEqual([
'middleware1:before',
'middleware2:before',
'middleware2:after',
'middleware1:after',
])
})
})
Considerations
- Test realistic combinations of components
- Focus on interaction points between components
- Use controlled test data that exercises integration points
- Verify both functional correctness and error handling
Related Patterns
- Composition Testing Pattern - Testing composed functions
- End-to-End Testing Pattern - Testing complete workflows
Mock Implementation Pattern
Pattern Overview
The Mock Implementation pattern creates controllable stand-ins for real dependencies.
Implementation Example
import { describe, test, expect, vi } from 'vitest'
import { createAdapter } from '@lib/content/adapters/base'
import { createEventEmitter } from '@lib/utils/events'
// Create mock adapter factory
export function createMockAdapter(options = {}) {
// Set up storage
const storage = new Map()
// Create controlled event emitter
const events = createEventEmitter()
// Default behavior
const behavior = {
// Control read behavior
readSuccess: true,
readDelay: 0,
// Control write behavior
writeSuccess: true,
writeDelay: 0,
// Control delete behavior
deleteSuccess: true,
deleteDelay: 0,
// Control list behavior
listSuccess: true,
listDelay: 0,
...options.behavior,
}
// Mock implementations with controlled behavior
const read = vi.fn(async uri => {
// Apply configured delay
if (behavior.readDelay > 0) {
await new Promise(resolve => setTimeout(resolve, behavior.readDelay))
}
// Fail if configured to do so
if (!behavior.readSuccess) {
throw new Error('Controlled read failure')
}
// Check if content exists
if (!storage.has(uri)) {
throw new ContentNotFoundError(uri)
}
// Return the content
return storage.get(uri)
})
const write = vi.fn(async (uri, content) => {
// Apply configured delay
if (behavior.writeDelay > 0) {
await new Promise(resolve => setTimeout(resolve, behavior.writeDelay))
}
// Fail if configured to do so
if (!behavior.writeSuccess) {
throw new Error('Controlled write failure')
}
// Store the content
storage.set(uri, { ...content })
// Emit change event
events.emit('change', {
uri,
content,
type: 'write',
timestamp: Date.now(),
})
})
const deleteOp = vi.fn(async uri => {
// Apply configured delay
if (behavior.deleteDelay > 0) {
await new Promise(resolve => setTimeout(resolve, behavior.deleteDelay))
}
// Fail if configured to do so
if (!behavior.deleteSuccess) {
throw new Error('Controlled delete failure')
}
// Check if content exists
if (!storage.has(uri)) {
throw new ContentNotFoundError(uri)
}
// Delete the content
storage.delete(uri)
// Emit change event
events.emit('change', {
uri,
content: null,
type: 'delete',
timestamp: Date.now(),
})
})
const list = vi.fn(async pattern => {
// Apply configured delay
if (behavior.listDelay > 0) {
await new Promise(resolve => setTimeout(resolve, behavior.listDelay))
}
// Fail if configured to do so
if (!behavior.listSuccess) {
throw new Error('Controlled list failure')
}
// Convert pattern to regex
const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`)
// Filter URIs by pattern
const uris = Array.from(storage.keys()).filter(uri => regex.test(uri))
return uris
})
// Helper to directly manipulate stored content (for test setup)
const setStoredContent = (uri, content) => {
storage.set(uri, { ...content })
}
// Helper to directly check stored content (for assertions)
const getStoredContent = uri => {
return storage.get(uri)
}
// Create the adapter
const adapter = createAdapter({
read,
write,
delete: deleteOp,
list,
events,
// Additional testing helpers
__test__: {
setStoredContent,
getStoredContent,
manipulateStorage: storage,
setBehavior: newBehavior => {
Object.assign(behavior, newBehavior)
},
resetBehavior: () => {
Object.assign(behavior, options.behavior || {})
},
},
})
return adapter
}
// Example mock adapter test
describe('MockAdapter', () => {
// Test mock adapter with controlled behavior
test('mock adapter returns controlled results', async () => {
// Create mock with default success behavior
const adapter = createMockAdapter()
// Setup test content
adapter.__test__.setStoredContent('test.md', {
data: 'Test content',
contentType: 'text/plain',
metadata: { title: 'Test' },
})
// Test: Read succeeds
const content = await adapter.read('test.md')
expect(content.data).toBe('Test content')
// Change behavior to simulate failure
adapter.__test__.setBehavior({ readSuccess: false })
// Test: Read now fails
await expect(adapter.read('test.md')).rejects.toThrow(
'Controlled read failure'
)
// Verify write was called with expected arguments
await adapter.write('new.md', {
data: 'New content',
contentType: 'text/plain',
})
expect(adapter.write).toHaveBeenCalledWith(
'new.md',
expect.objectContaining({ data: 'New content' })
)
})
// Test delays
test('mock adapter honors configured delays', async () => {
// Create mock with significant delay
const adapter = createMockAdapter({
behavior: { readDelay: 100 },
})
// Setup test content
adapter.__test__.setStoredContent('test.md', {
data: 'Test content',
contentType: 'text/plain',
})
// Test: Measure operation time
const start = Date.now()
await adapter.read('test.md')
const duration = Date.now() - start
// Assert: Operation took at least the configured delay
expect(duration).toBeGreaterThanOrEqual(100)
})
})
Considerations
- Make mocks controllable with explicit behavior settings
- Include both success and failure modes in mock implementations
- Add testing-specific helpers that aren’t part of the real API
- Document mock behavior clearly for test authors
Related Patterns
- Test Double Pattern - Different types of test replacements
- Stubbing Pattern - Returning predefined responses
Environment Simulation Pattern
Pattern Overview
The Environment Simulation pattern creates virtual environments for testing environment-specific code.
Implementation Example
import { describe, test, expect, vi } from 'vitest'
import { createOptimalAdapter } from '@lib/content/adapters/factory'
import { isNode, isBrowser, isServiceWorker } from '@lib/utils/env'
// Mock environment detection
vi.mock('@lib/utils/env', () => ({
isNode: vi.fn(() => false),
isBrowser: vi.fn(() => false),
isServiceWorker: vi.fn(() => false),
hasIndexedDB: vi.fn(() => false),
hasLocalStorage: vi.fn(() => false),
}))
// Mock implementations
const mockFileSystemAdapter = vi.fn(() => ({ type: 'filesystem' }))
const mockIndexedDBAdapter = vi.fn(() => ({ type: 'indexeddb' }))
const mockLocalStorageAdapter = vi.fn(() => ({ type: 'localstorage' }))
const mockServiceWorkerAdapter = vi.fn(() => ({ type: 'serviceworker' }))
const mockMemoryAdapter = vi.fn(() => ({ type: 'memory' }))
vi.mock('@lib/content/adapters/node/filesystem', () => ({
createFilesystemAdapter: mockFileSystemAdapter,
}))
vi.mock('@lib/content/adapters/browser/indexed-db', () => ({
createIndexedDBAdapter: mockIndexedDBAdapter,
}))
vi.mock('@lib/content/adapters/browser/local-storage', () => ({
createLocalStorageAdapter: mockLocalStorageAdapter,
}))
vi.mock('@lib/content/adapters/browser/service-worker', () => ({
createServiceWorkerAdapter: mockServiceWorkerAdapter,
}))
vi.mock('@lib/content/adapters/common/memory', () => ({
createMemoryAdapter: mockMemoryAdapter,
}))
// Environment simulation tests
describe('OptimalAdapter environment detection', () => {
// Reset mocks between tests
beforeEach(() => {
vi.resetAllMocks()
// Default all environments to false
isNode.mockReturnValue(false)
isBrowser.mockReturnValue(false)
isServiceWorker.mockReturnValue(false)
})
// Test Node.js environment
test('selects filesystem adapter in Node.js environment', () => {
// Simulate Node.js environment
isNode.mockReturnValue(true)
// Create adapter
const adapter = createOptimalAdapter()
// Verify correct adapter was created
expect(mockFileSystemAdapter).toHaveBeenCalled()
expect(adapter.type).toBe('filesystem')
})
// Test browser environment with IndexedDB
test('selects IndexedDB adapter in browser with IndexedDB support', () => {
// Simulate browser environment with IndexedDB
isBrowser.mockReturnValue(true)
// Mock feature detection
const { hasIndexedDB } = require('@lib/utils/env')
hasIndexedDB.mockReturnValue(true)
// Create adapter
const adapter = createOptimalAdapter()
// Verify correct adapter was created
expect(mockIndexedDBAdapter).toHaveBeenCalled()
expect(adapter.type).toBe('indexeddb')
})
// Test browser environment with localStorage fallback
test('selects localStorage adapter in browser without IndexedDB', () => {
// Simulate browser environment without IndexedDB
isBrowser.mockReturnValue(true)
// Mock feature detection
const { hasIndexedDB, hasLocalStorage } = require('@lib/utils/env')
hasIndexedDB.mockReturnValue(false)
hasLocalStorage.mockReturnValue(true)
// Create adapter
const adapter = createOptimalAdapter()
// Verify correct adapter was created
expect(mockLocalStorageAdapter).toHaveBeenCalled()
expect(adapter.type).toBe('localstorage')
})
// Test service worker environment
test('selects service worker adapter in service worker environment', () => {
// Simulate service worker environment
isServiceWorker.mockReturnValue(true)
// Create adapter
const adapter = createOptimalAdapter()
// Verify correct adapter was created
expect(mockServiceWorkerAdapter).toHaveBeenCalled()
expect(adapter.type).toBe('serviceworker')
})
// Test fallback for unknown environment
test('falls back to memory adapter for unknown environment', () => {
// All environment detections are false
// Create adapter
const adapter = createOptimalAdapter()
// Verify fallback adapter was created
expect(mockMemoryAdapter).toHaveBeenCalled()
expect(adapter.type).toBe('memory')
})
})
Considerations
- Mock environment detection utilities to simulate different environments
- Test all supported environments and fallback paths
- Maintain isolation between environment-specific tests
- Reset mocked state between tests to prevent cross-test contamination
Related Patterns
- Platform Abstraction Pattern - Abstracting platform-specific APIs
- Capability Detection Pattern - Testing feature detection logic
Test Data Factory Pattern
Pattern Overview
The Test Data Factory pattern provides consistent test data for different test scenarios.
Implementation Example
/**
* Test data factories for content system tests
*/
// Factory for content items
export function createTestContent(overrides = {}) {
return {
data: 'Default test content',
contentType: 'text/plain',
metadata: {
title: 'Test Content',
created: '2025-01-01T00:00:00.000Z',
modified: '2025-01-01T00:00:00.000Z',
...overrides.metadata,
},
...overrides,
}
}
// Factory for markdown content
export function createMarkdownContent(overrides = {}) {
return createTestContent({
data: '# Test Markdown\n\nThis is test markdown content.',
contentType: 'text/markdown',
metadata: {
title: 'Test Markdown',
...overrides.metadata,
},
...overrides,
})
}
// Factory for JSON content
export function createJsonContent(overrides = {}) {
return createTestContent({
data: JSON.stringify({ test: true, value: 42 }),
contentType: 'application/json',
metadata: {
title: 'Test JSON',
...overrides.metadata,
},
...overrides,
})
}
// Factory for binary content
export function createBinaryContent(overrides = {}) {
// Create binary data (Uint8Array)
const data = new Uint8Array([0x54, 0x65, 0x73, 0x74]) // "Test" in ASCII
return createTestContent({
data,
contentType: 'application/octet-stream',
metadata: {
title: 'Test Binary',
...overrides.metadata,
},
...overrides,
})
}
// Factory for creating multiple content items
export function createTestContentSet(count = 5, factory = createTestContent) {
const result = {}
for (let i = 0; i < count; i++) {
const uri = `test-${i}.${factory === createMarkdownContent ? 'md' : 'txt'}`
result[uri] = factory({
metadata: {
title: `Test ${i}`,
index: i,
},
})
}
return result
}
// Factory for content store operations
export function createTestOperations(uris = ['test.md']) {
return {
// Basic read operations
read: uris.map(uri => ({
type: 'read',
uri,
options: {},
})),
// Basic write operations
write: uris.map(uri => ({
type: 'write',
uri,
content: createTestContent({
metadata: { title: `Content for ${uri}` },
}),
options: {},
})),
// Write with different content types
writeMixed: [
{
type: 'write',
uri: 'test.md',
content: createMarkdownContent(),
options: {},
},
{
type: 'write',
uri: 'test.json',
content: createJsonContent(),
options: {},
},
{
type: 'write',
uri: 'test.bin',
content: createBinaryContent(),
options: {},
},
],
// Delete operations
delete: uris.map(uri => ({
type: 'delete',
uri,
options: {},
})),
// List operations
list: [
{ type: 'list', pattern: '**', options: {} },
{ type: 'list', pattern: '*.md', options: {} },
{ type: 'list', pattern: 'test/*', options: {} },
],
}
}
// Factory for error cases
export function createErrorScenarios() {
return {
// Not found errors
notFound: {
type: 'read',
uri: 'does-not-exist.md',
expectError: 'ContentNotFoundError',
},
// Validation errors
invalidContent: {
type: 'write',
uri: 'invalid.md',
content: {
/* Missing required fields */
},
expectError: 'ContentValidationError',
},
// Access errors
accessDenied: {
type: 'write',
uri: 'forbidden/test.md',
content: createTestContent(),
expectError: 'ContentAccessError',
},
// Format errors
invalidFormat: {
type: 'write',
uri: 'invalid-json.json',
content: createTestContent({
data: '{ invalid json',
contentType: 'application/json',
}),
expectError: 'ContentFormatError',
},
}
}
// Usage example in tests
describe('ContentStore operations', () => {
test('successfully reads and writes content', async () => {
// Create store
const store = createContentStore()
// Get test operation and data
const operations = createTestOperations(['test.md'])
const content = createMarkdownContent()
// Write test content
await store.write('test.md', content)
// Read and verify
const result = await store.read('test.md')
expect(result).toEqual(content)
})
test('handles different content types correctly', async () => {
// Create store
const store = createContentStore()
// Write mixed content types
const operations = createTestOperations().writeMixed
for (const op of operations) {
await store.write(op.uri, op.content)
}
// Verify each type
const markdown = await store.read('test.md')
expect(markdown.contentType).toBe('text/markdown')
const json = await store.read('test.json')
expect(json.contentType).toBe('application/json')
const binary = await store.read('test.bin')
expect(binary.contentType).toBe('application/octet-stream')
})
})
Considerations
- Create factories for common data structures needed in tests
- Allow customization through overrides with sensible defaults
- Create specialized factories for different data variations
- Use factories to reduce duplication and improve test maintainability
Related Patterns
- Object Mother Pattern - Creating complex test objects
- Builder Pattern - Fluent interface for test object creation
Asynchronous Testing Pattern
Pattern Overview
The Asynchronous Testing pattern validates operations that involve promises, timeouts, and events.
Implementation Example
import { describe, test, expect, vi } from 'vitest'
import { createMemoryAdapter } from '@lib/content/adapters/common/memory'
import { createContentStore } from '@lib/content/store/factory'
// Setup timer and promise mocking
vi.useFakeTimers()
describe('Asynchronous content operations', () => {
let adapter
let store
beforeEach(() => {
adapter = createMemoryAdapter()
store = createContentStore({ adapter })
})
// Test async event handling
test('listen for content changes', async () => {
// Setup change listener with mock
const changeListener = vi.fn()
const unsubscribe = store.onChange(changeListener)
// Verify no initial calls
expect(changeListener).not.toHaveBeenCalled()
// Make a change
await store.write('test.md', {
data: 'Test content',
contentType: 'text/markdown',
})
// Verify listener was called
expect(changeListener).toHaveBeenCalledTimes(1)
expect(changeListener).toHaveBeenCalledWith(
'test.md',
expect.objectContaining({
data: 'Test content',
contentType: 'text/markdown',
}),
'write'
)
// Unsubscribe and verify no more calls
unsubscribe()
await store.write('test2.md', {
data: 'More content',
contentType: 'text/markdown',
})
expect(changeListener).toHaveBeenCalledTimes(1)
})
// Test throttling and debouncing
test('throttled operations respect timing', async () => {
// Create throttled operation (100ms)
const throttledWrite = throttle(store.write.bind(store), 100)
// Execute multiple times in rapid succession
throttledWrite('test.md', { data: 'A', contentType: 'text/plain' })
throttledWrite('test.md', { data: 'B', contentType: 'text/plain' })
throttledWrite('test.md', { data: 'C', contentType: 'text/plain' })
// Fast-forward time by 50ms (not enough for throttle)
vi.advanceTimersByTime(50)
// Only the first call should have executed
const contentAfter50ms = await store.read('test.md')
expect(contentAfter50ms.data).toBe('A')
// Fast-forward another 100ms (enough for another throttled call)
vi.advanceTimersByTime(100)
// The last queued call should now have executed
const contentAfter150ms = await store.read('test.md')
expect(contentAfter150ms.data).toBe('C')
})
// Test concurrent operations
test('handles concurrent operations correctly', async () => {
// Start multiple operations concurrently
const operations = [
store.write('test1.md', { data: 'Content 1', contentType: 'text/plain' }),
store.write('test2.md', { data: 'Content 2', contentType: 'text/plain' }),
store.write('test3.md', { data: 'Content 3', contentType: 'text/plain' }),
store.read('test1.md').catch(() => null),
]
// Wait for all to complete
await Promise.all(operations)
// Verify results
const results = await Promise.all([
store.read('test1.md'),
store.read('test2.md'),
store.read('test3.md'),
])
expect(results[0].data).toBe('Content 1')
expect(results[1].data).toBe('Content 2')
expect(results[2].data).toBe('Content 3')
})
// Test timeouts
test('operations timeout after specified time', async () => {
// Mock adapter read to delay
const originalRead = adapter.read
adapter.read = vi.fn(async uri => {
// Wait for 5 seconds (longer than timeout)
await new Promise(resolve => setTimeout(resolve, 5000))
return originalRead(uri)
})
// Create store with short timeout
const timeoutStore = createContentStore({
adapter,
timeout: 1000, // 1 second timeout
})
// Start operation
const operation = timeoutStore.read('test.md')
// Fast-forward past timeout
vi.advanceTimersByTime(2000)
// Verify operation timed out
await expect(operation).rejects.toThrow('Operation timed out')
})
})
// Simple throttle implementation for testing
function throttle(fn, delay) {
let lastCall = 0
let timeout = null
let lastArgs = null
return function throttled(...args) {
const now = Date.now()
// Clear any pending timeouts
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// If enough time has passed, execute immediately
if (now - lastCall >= delay) {
lastCall = now
return fn(...args)
}
// Otherwise, schedule for later
lastArgs = args
timeout = setTimeout(
() => {
lastCall = Date.now()
timeout = null
fn(...lastArgs)
},
delay - (now - lastCall)
)
}
}
Considerations
- Control time with fake timers for predictable testing
- Test concurrent operations with Promise.all and race conditions
- Verify event listeners are properly registered and cleaned up
- Use isolation to prevent test interference with async operations
Related Patterns
- Race Condition Testing Pattern - Testing timing-sensitive code
- Promise Testing Pattern - Testing asynchronous promises
Snapshot Testing Pattern
Pattern Overview
The Snapshot Testing pattern verifies that complex objects match expected structures without manual assertions.
Implementation Example
import { describe, test, expect } from 'vitest'
import { createContentStore } from '@lib/content/store/factory'
import { createMemoryAdapter } from '@lib/content/adapters/common/memory'
import { withTransformers } from '@lib/content/middleware/transformers'
// Test content transformations with snapshots
describe('Content transformations', () => {
let store
beforeEach(async () => {
// Create store with transformers
store = createContentStore({
adapter: createMemoryAdapter(),
enhancers: [
withTransformers({
'text/markdown': [
// Add transformers for markdown
extractFrontmatter,
parseMarkdown,
enhanceLinks,
],
'application/json': [
// Add transformers for JSON
parseJson,
validateJson,
addMetadata,
],
}),
],
})
// Set up test content
await store.write('document.md', {
data: `---
title: Test Document
author: Test Author
date: 2025-01-01
---
# Test Document
This is a [test link](https://example.com).
`,
contentType: 'text/markdown',
})
await store.write('data.json', {
data: `{
"id": 123,
"name": "Test Item",
"properties": {
"color": "blue",
"size": "medium"
}
}`,
contentType: 'application/json',
})
})
// Test markdown transformation with snapshot
test('transforms markdown content correctly', async () => {
// Read and transform content
const content = await store.read('document.md')
// Verify transformation result matches snapshot
expect(content).toMatchSnapshot({
metadata: expect.any(Object), // Metadata can vary between test runs
})
})
// Test JSON transformation with snapshot
test('transforms JSON content correctly', async () => {
// Read and transform content
const content = await store.read('data.json')
// Verify transformation result matches snapshot
expect(content).toMatchSnapshot({
metadata: {
...expect.any(Object),
processedAt: expect.any(String), // Ignore specific timestamp
},
})
})
// Test multiple transformations at once
test('all transformations produce consistent output', async () => {
// Read all content
const markdown = await store.read('document.md')
const json = await store.read('data.json')
// Create a combined snapshot
expect({
markdown: {
...markdown,
metadata: { ...markdown.metadata, processedAt: '[timestamp]' },
},
json: {
...json,
metadata: { ...json.metadata, processedAt: '[timestamp]' },
},
}).toMatchSnapshot()
})
})
// Mock transformers
function extractFrontmatter(content) {
// Simple frontmatter extraction
const match = content.data.match(/^---\n([\s\S]*?)\n---\n/)
if (match) {
const frontmatter = match[1]
const lines = frontmatter.split('\n')
const metadata = {}
lines.forEach(line => {
const [key, value] = line.split(': ')
if (key && value) {
metadata[key.trim()] = value.trim()
}
})
return {
...content,
data: content.data.replace(/^---\n[\s\S]*?\n---\n/, ''),
metadata: {
...content.metadata,
...metadata,
},
}
}
return content
}
function parseMarkdown(content) {
// Mock markdown parsing (would use a real parser in production)
return {
...content,
processed: {
...content.processed,
headings: content.data.match(/^#+\s+(.*)$/gm) || [],
links: (content.data.match(/\[([^\]]+)\]\(([^)]+)\)/g) || []).map(
link => {
const match = link.match(/\[([^\]]+)\]\(([^)]+)\)/)
return { text: match[1], url: match[2] }
}
),
},
}
}
function enhanceLinks(content) {
// Add metadata about links
return {
...content,
metadata: {
...content.metadata,
linkCount: (content.processed?.links || []).length,
processedAt: new Date().toISOString(),
},
}
}
function parseJson(content) {
// Parse JSON string to object
return {
...content,
processed: {
...content.processed,
parsed: JSON.parse(content.data),
},
}
}
function validateJson(content) {
// Simply mark as valid in this test
return {
...content,
processed: {
...content.processed,
valid: true,
},
}
}
function addMetadata(content) {
// Add metadata based on JSON content
const parsed = content.processed?.parsed
return {
...content,
metadata: {
...content.metadata,
keys: Object.keys(parsed || {}),
hasNested: !!Object.values(parsed || {}).find(v => typeof v === 'object'),
processedAt: new Date().toISOString(),
},
}
}
Considerations
- Use snapshots for complex object structures that are tedious to assert manually
- Update snapshots when intentional changes are made
- Control non-deterministic values (like timestamps) in snapshots
- Keep snapshots focused on relevant parts of the output
Related Patterns
- Golden Master Pattern - Comparing against known-good output
- Structure Verification Pattern - Verifying object structures
Environment Setup Pattern
Pattern Overview
The Environment Setup pattern establishes predictable test environments.
Implementation Example
/**
* Test environment setup for content system tests
*/
import { vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
import { createMemoryAdapter } from '@lib/content/adapters/common/memory'
import { createFilesystemAdapter } from '@lib/content/adapters/node/filesystem'
import { createContentStore } from '@lib/content/store/factory'
import path from 'path'
import fs from 'fs/promises'
// Setup environment variables
beforeAll(() => {
// Set test environment variables
process.env.CONTENT_STORE_TYPE = 'memory'
process.env.CONTENT_STORE_LOG_LEVEL = 'error'
process.env.TEST_MODE = 'true'
// Disable console output during tests
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'info').mockImplementation(() => {})
})
// Cleanup
afterAll(() => {
// Restore environment variables
delete process.env.CONTENT_STORE_TYPE
delete process.env.CONTENT_STORE_LOG_LEVEL
delete process.env.TEST_MODE
// Restore console
vi.restoreAllMocks()
})
// Global adapter and store instances for memory-based tests
export function createTestMemoryStore(options = {}) {
const adapter = createMemoryAdapter()
const store = createContentStore({
adapter,
...options,
})
return { adapter, store }
}
// Filesystem-based test store with temporary directory
export async function createTestFilesystemStore(options = {}) {
// Create temporary directory
const testDir = path.join(
process.cwd(),
'tmp',
'test',
`content-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
)
// Ensure directory exists
await fs.mkdir(testDir, { recursive: true })
// Create adapter and store
const adapter = createFilesystemAdapter({
basePath: testDir,
...options.adapter,
})
const store = createContentStore({
adapter,
...options.store,
})
// Return store with cleanup function
return {
adapter,
store,
testDir,
async cleanup() {
// Dispose store and adapter
await store.dispose()
// Remove temporary directory
await fs.rm(testDir, { recursive: true, force: true })
},
}
}
// Example usage in test file
describe('Content operations with filesystem storage', () => {
let testEnv
beforeEach(async () => {
// Create test environment
testEnv = await createTestFilesystemStore()
})
afterEach(async () => {
// Cleanup test environment
await testEnv.cleanup()
})
test('writes and reads content from filesystem', async () => {
const { store } = testEnv
// Write content
await store.write('test.md', {
data: '# Test',
contentType: 'text/markdown',
metadata: { title: 'Test' },
})
// Verify exists on disk
const filePath = path.join(testEnv.testDir, 'test.md')
const exists = await fs
.access(filePath)
.then(() => true)
.catch(() => false)
expect(exists).toBe(true)
// Read content
const content = await store.read('test.md')
expect(content.data).toContain('# Test')
})
})
Considerations
- Create isolated environments for each test to prevent cross-test contamination
- Provide cleanup functions to ensure resources are released
- Seed test environments with predictable initial state
- Mock external dependencies for faster and more reliable tests
Related Patterns
- Test Fixture Pattern - Reusable test environments
- Dependency Injection Pattern - Providing test dependencies