UNPKG

@seedts/testing

Version:

Testing utilities for SeedTS - Snapshot testing, deterministic seeding, and test helpers

569 lines (432 loc) โ€ข 14.3 kB
# @seedts/testing Testing utilities for SeedTS - Snapshot testing, deterministic seeding, and test helpers. ## Installation ```bash npm install --save-dev @seedts/testing # or pnpm add -D @seedts/testing # or yarn add --dev @seedts/testing ``` ## Features - ๐Ÿ“ธ **Snapshot Testing** - Create and compare snapshots of seeded data - ๐ŸŽฒ **Deterministic Seeding** - Generate consistent data for reliable tests - ๐Ÿงช **Test Helpers** - Utilities for asserting and validating seed results - โšก **Framework Integration** - Jest and Vitest support - ๐Ÿ”ง **Flexible** - Works with any test framework ## Quick Start ### Snapshot Testing ```typescript import { snapshot } from '@seedts/testing'; import { Executor } from '@seedts/core'; test('users seed generates correct data', async () => { const executor = new Executor([UsersSeed]); const results = await executor.execute(); const result = await snapshot('users', results[0].data); expect(result.pass).toBe(true); }); ``` ### Deterministic Seeding ```typescript import { withDeterministicContext } from '@seedts/testing'; test('users seed with deterministic data', async () => { await withDeterministicContext(async (ctx) => { // Math.random and Date.now() are now deterministic const executor = new Executor([UsersSeed]); const results = await executor.execute(); // Results will be identical on every run expect(results[0].data[0].createdAt).toEqual(new Date('2024-01-01T00:00:00Z')); }, { seed: 42 }); }); ``` ## Snapshot Testing ### Creating Snapshots ```typescript import { snapshot } from '@seedts/testing'; const result = await snapshot('users', usersData, { snapshotDir: '__snapshots__', pretty: true }); if (result.isNew) { console.log('New snapshot created:', result.snapshotPath); } ``` ### Comparing Snapshots ```typescript const result = await snapshot('users', usersData); if (!result.pass) { console.error('Snapshot mismatch:'); console.error(result.diff?.diff); } ``` ### Updating Snapshots ```typescript // Programmatically const result = await snapshot('users', usersData, { updateSnapshot: true }); // Via environment variable // UPDATE_SNAPSHOTS=true npm test ``` ### Snapshot Options ```typescript interface SnapshotOptions { snapshotDir?: string; // Default: '__snapshots__' snapshotName?: string; // Default: seed name pretty?: boolean; // Default: true updateSnapshot?: boolean; // Default: false excludeFields?: string[]; // Fields to exclude serializer?: (data: any) => string; // Custom serializer normalize?: (data: any[]) => any[]; // Data normalizer } ``` ### Normalizing Data ```typescript import { snapshot, normalizeSeedResult } from '@seedts/testing'; // Remove timestamps and sort by ID for consistent snapshots const result = await snapshot('users', usersData, { normalize: normalizeSeedResult }); // Custom normalizer const result = await snapshot('products', productsData, { normalize: (data) => data.sort((a, b) => a.sku.localeCompare(b.sku)) }); ``` ### Excluding Fields ```typescript const result = await snapshot('users', usersData, { excludeFields: ['createdAt', 'updatedAt', 'id'] }); ``` ## Deterministic Seeding ### Seeded Random Number Generator ```typescript import { SeededRandom } from '@seedts/testing'; const random = new SeededRandom(42); random.next(); // 0-1 random number random.int(1, 100); // Random integer 1-100 random.float(0, 1); // Random float 0-1 random.pick(['a', 'b', 'c']); // Random array element random.shuffle([1, 2, 3]); // Shuffle array random.boolean(); // Random boolean ``` ### Deterministic Context ```typescript import { createDeterministicContext } from '@seedts/testing'; const ctx = createDeterministicContext({ seed: 42, freezeTime: true, timestamp: new Date('2024-01-01T00:00:00Z') }); // Use in factories const factory = () => ({ name: ctx.random.pick(['Alice', 'Bob', 'Charlie']), age: ctx.random.int(18, 80), createdAt: ctx.getDate(), score: ctx.random.float(0, 100) }); // Always restore after use ctx.restore(); ``` ### Deterministic Execution ```typescript import { withDeterministicContext } from '@seedts/testing'; await withDeterministicContext(async (ctx) => { // Everything inside is deterministic const data = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, value: ctx.random.int(1, 100), timestamp: ctx.getDate() })); return data; }, { seed: 42 }); // Context automatically restored ``` ### Deterministic Faker ```typescript import { setupDeterministicFaker } from '@seedts/testing'; import { faker } from '@faker-js/faker'; const { restore } = setupDeterministicFaker(42); // Faker will generate consistent data const name = faker.person.fullName(); // Always same for seed 42 const email = faker.internet.email(); // Always same for seed 42 restore(); // Restore Math.random ``` ### Deterministic Generators ```typescript import { DeterministicIdGenerator, createDeterministicUUID, createDeterministicEmail, createDeterministicUsername } from '@seedts/testing'; // ID generator const idGen = new DeterministicIdGenerator(1); idGen.next(); // 1 idGen.next(); // 2 idGen.next(); // 3 // UUID generator const uuidGen = createDeterministicUUID(); uuidGen(); // '00000000-0000-0000-0000-000000000001' uuidGen(); // '00000000-0000-0000-0000-000000000002' // Email generator const emailGen = createDeterministicEmail(); emailGen(); // 'user0@example.com' emailGen(); // 'user1@example.com' // Username generator const usernameGen = createDeterministicUsername(); usernameGen(); // 'user0' usernameGen(); // 'user1' ``` ## Test Helpers ### Execution Helpers ```typescript import { executeSeedsAsMap } from '@seedts/testing'; const executor = new Executor([UsersSeed, PostsSeed]); const results = await executeSeedsAsMap(executor); // Access by name const users = results.get('users'); const posts = results.get('posts'); ``` ### Assertions ```typescript import { assertSeedSuccess, assertAllSeedsSuccess, assertSeedCount, assertSeedData } from '@seedts/testing'; const results = await executor.execute(); // Assert single seed succeeded assertSeedSuccess(results[0]); // Assert all seeds succeeded assertAllSeedsSuccess(results); // Assert record count assertSeedCount(results[0], 10); // Assert data validity assertSeedData(results[0], (user) => { return user.email.includes('@') && user.age >= 18; }); ``` ### Data Validation ```typescript import { createValidator, assertSeedData } from '@seedts/testing'; const validateUser = createValidator<User>({ email: (v) => typeof v === 'string' && v.includes('@'), age: (v) => typeof v === 'number' && v >= 0 && v <= 150, name: (v) => typeof v === 'string' && v.length > 0 }); assertSeedData(usersResult, validateUser); ``` ### Performance Assertions ```typescript import { assertSeedDuration } from '@seedts/testing'; const results = await executor.execute(); // Assert completed within 1 second assertSeedDuration(results[0], 1000); ``` ## Test Framework Integration ### Vitest Integration ```typescript import { expect, describe, it } from 'vitest'; import { extendVitestMatchers } from '@seedts/testing/vitest'; // Extend Vitest matchers extendVitestMatchers(expect); describe('Seed Snapshots', () => { it('users seed matches snapshot', async () => { const executor = new Executor([UsersSeed]); const results = await executor.execute(); await expect(results[0].data).toMatchSeedSnapshot('users'); }); }); ``` ### Jest Integration ```typescript import { expect, describe, it } from '@jest/globals'; import { extendJestMatchers } from '@seedts/testing/jest'; // Extend Jest matchers extendJestMatchers(expect); describe('Seed Snapshots', () => { it('users seed matches snapshot', async () => { const executor = new Executor([UsersSeed]); const results = await executor.execute(); await expect(results[0].data).toMatchSeedSnapshot('users'); }); }); ``` ### Updating Snapshots ```bash # Vitest UPDATE_SNAPSHOTS=true npm test # Jest npm test -- --updateSnapshot # or npm test -- -u ``` ## Complete Examples ### Example 1: Snapshot Testing with Deterministic Data ```typescript import { describe, it, expect } from 'vitest'; import { Executor } from '@seedts/core'; import { MemoryAdapter } from '@seedts/memory'; import { withDeterministicContext, snapshot, normalizeSeedResult } from '@seedts/testing'; describe('Users Seed', () => { it('generates consistent snapshot', async () => { await withDeterministicContext(async () => { const adapter = new MemoryAdapter(); const executor = new Executor([ (props) => UsersSeed({ ...props, adapter }) ]); const results = await executor.execute(); const usersData = results[0].data; const result = await snapshot('users-deterministic', usersData, { normalize: normalizeSeedResult, excludeFields: ['id'] }); expect(result.pass).toBe(true); }, { seed: 42 }); }); }); ``` ### Example 2: Testing Multiple Seeds ```typescript import { Executor, executeSeedsAsMap, assertAllSeedsSuccess, assertSeedCount } from '@seedts/testing'; describe('All Seeds', () => { it('execute successfully', async () => { const adapter = new MemoryAdapter(); const executor = new Executor([ (props) => UsersSeed({ ...props, adapter }), (props) => PostsSeed({ ...props, adapter }), (props) => CommentsSeed({ ...props, adapter }) ]); const results = await executeSeedsAsMap(executor); // Assert all succeeded assertAllSeedsSuccess(Array.from(results.values())); // Assert counts assertSeedCount(results.get('users')!, 10); assertSeedCount(results.get('posts')!, 25); assertSeedCount(results.get('comments')!, 100); }); }); ``` ### Example 3: Deterministic Faker Integration ```typescript import { setupDeterministicFaker } from '@seedts/testing'; import { faker } from '@faker-js/faker'; describe('Deterministic Faker', () => { it('generates consistent data', () => { const { restore } = setupDeterministicFaker(42); const users = Array.from({ length: 5 }, () => ({ name: faker.person.fullName(), email: faker.internet.email(), age: faker.number.int({ min: 18, max: 80 }) })); // These will be the same on every run expect(users[0].name).toBe('Expected Name'); // Based on seed 42 expect(users.length).toBe(5); restore(); }); }); ``` ### Example 4: Custom Validators ```typescript import { createValidator, assertSeedData } from '@seedts/testing'; describe('Data Validation', () => { it('validates user data structure', async () => { const validateUser = createValidator<User>({ id: (v) => typeof v === 'number' && v > 0, email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), age: (v) => Number.isInteger(v) && v >= 18 && v <= 120, name: (v) => typeof v === 'string' && v.length >= 2, createdAt: (v) => v instanceof Date || typeof v === 'string' }); const executor = new Executor([UsersSeed]); const results = await executor.execute(); assertSeedData(results[0], validateUser); }); }); ``` ### Example 5: Snapshot Diff Workflow ```typescript import { snapshot } from '@seedts/testing'; describe('Snapshot Workflow', () => { it('handles snapshot lifecycle', async () => { const data = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; // First run - creates snapshot let result = await snapshot('test-data', data); expect(result.isNew).toBe(true); // Second run - matches snapshot result = await snapshot('test-data', data); expect(result.pass).toBe(true); // Modified data - shows diff const modifiedData = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Charlie' } // Changed from Bob ]; result = await snapshot('test-data', modifiedData); expect(result.pass).toBe(false); expect(result.diff).toBeDefined(); console.log(result.diff?.diff); // Update snapshot result = await snapshot('test-data', modifiedData, { updateSnapshot: true }); expect(result.wasUpdated).toBe(true); }); }); ``` ## API Reference ### Snapshot Functions - `snapshot(name, data, options?)` - Create or compare snapshot - `updateSnapshot(name, data, options?)` - Update existing snapshot - `deleteSnapshot(name, options?)` - Delete snapshot file - `listSnapshots(dir?)` - List all snapshots - `clearSnapshots(dir?)` - Clear all snapshots - `createSnapshotAssertion(data, testName, options?)` - Create assertion helper - `shouldUpdateSnapshots()` - Check if UPDATE_SNAPSHOTS=true - `normalizeSeedResult(data)` - Normalize data for snapshots ### Deterministic Functions - `SeededRandom(seed?)` - Seeded random number generator class - `DeterministicContext(options?)` - Context with frozen time and seeded random - `createDeterministicContext(options?)` - Create deterministic context - `withDeterministicContext(fn, options?)` - Execute with deterministic context - `setupDeterministicFaker(seed?)` - Make Faker.js deterministic - `DeterministicIdGenerator(start?)` - Sequential ID generator - `createDeterministicUUID()` - Deterministic UUID generator - `createDeterministicEmail()` - Deterministic email generator - `createDeterministicUsername()` - Deterministic username generator ### Test Helpers - `executeSeedsAsMap(executor)` - Execute and return results as map - `assertSeedSuccess(result)` - Assert seed succeeded - `assertAllSeedsSuccess(results)` - Assert all seeds succeeded - `getSeedByName(results, name)` - Get seed by name - `assertSeedCount(result, count)` - Assert record count - `assertSeedData(result, predicate)` - Assert data validity - `assertSeedDuration(result, maxMs)` - Assert execution time - `createValidator(rules)` - Create data validator - `waitFor(condition, timeout?, interval?)` - Wait for condition - `deepEqual(a, b)` - Deep equality comparison ## TypeScript Support All functions are fully typed: ```typescript import type { SnapshotOptions, SnapshotResult, SnapshotDiff, DeterministicOptions } from '@seedts/testing'; ``` ## License MIT