npm package: @seedts/testing
Description: Testing utilities for SeedTS - Snapshot testing, deterministic seeding, and test helpers
Author: SeedTS Contributors
Latest version: 0.1.1

# @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