@seedts/testing
Version:
Testing utilities for SeedTS - Snapshot testing, deterministic seeding, and test helpers
569 lines (432 loc) โข 14.3 kB
Markdown
# /testing
Testing utilities for SeedTS - Snapshot testing, deterministic seeding, and test helpers.
## Installation
```bash
npm install --save-dev /testing
# or
pnpm add -D /testing
# or
yarn add --dev /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