Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Continuous Integration

on:
push:
branches: main
pull_request:
branches: main

jobs:
test:
name: Test ${{ matrix.example }} (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
timeout-minutes: 10

strategy:
fail-fast: false
matrix:
example:
- advanced-mock-config
- inversify-jest
- inversify-sinon
- inversify-vitest
- nestjs-jest
- nestjs-sinon
- nestjs-vitest
node-version:
- '18'
- '20'
- '22'

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9.15.4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: ${{ matrix.example }}/pnpm-lock.yaml

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ${{ matrix.example }}/node_modules
key: modules-${{ runner.os }}-node${{ matrix.node-version }}-${{ matrix.example }}-${{ hashFiles(format('{0}/pnpm-lock.yaml', matrix.example)) }}
restore-keys: |
modules-${{ runner.os }}-node${{ matrix.node-version }}-${{ matrix.example }}-

- name: Install dependencies
working-directory: ${{ matrix.example }}
run: pnpm install --frozen-lockfile

- name: Run tests
working-directory: ${{ matrix.example }}
run: pnpm test

- name: Report results
if: always()
run: |
echo "### ${{ matrix.example }} (Node ${{ matrix.node-version }})" >> $GITHUB_STEP_SUMMARY
echo "Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
158 changes: 158 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Suites Examples

Real-world examples demonstrating [Suites](https://suites.dev) integration with popular dependency injection frameworks and test runners.

Each example showcases [solitary](https://suites.dev/docs/api-reference/testbed-solitary) and [sociable](https://suites.dev/docs/api-reference/testbed-sociable) testing patterns using the same user management domain model.

If you are new to Suites, check out the [Getting Started](https://suites.dev/docs/getting-started) guide.

## Examples

| Example | DI Framework | Test Runner | Use When |
| ---------------------------------------------- | ------------ | ----------- | ------------------------------------------------- |
| [nestjs-jest](./nestjs-jest) | NestJS | Jest | NestJS with Jest |
| [nestjs-vitest](./nestjs-vitest) | NestJS | Vitest | NestJS with Vitest |
| [nestjs-sinon](./nestjs-sinon) | NestJS | Sinon | NestJS with Sinon/Mocha |
| [inversify-jest](./inversify-jest) | InversifyJS | Jest | InversifyJS with Jest |
| [inversify-vitest](./inversify-vitest) | InversifyJS | Vitest | InversifyJS with Vitest |
| [inversify-sinon](./inversify-sinon) | InversifyJS | Sinon | InversifyJS with Sinon/Mocha |
| [advanced-mock-config](./advanced-mock-config) | NestJS | Jest | Advanced `.mock().final()` and `.impl()` patterns |

## Quick Start

```bash
# Clone and run any example
cd nestjs-jest
pnpm install
pnpm test
```

All tests should pass immediately, demonstrating both testing strategies.

## Testing Strategies

Each example demonstrates two approaches:

### Solitary Unit Tests

```typescript
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
```

Test one class in complete isolation. All dependencies are replaced with test doubles.

**When to use:**

- Testing component logic in isolation
- Controlling all inputs for predictable results

**Trade-off:** Does not verify interactions between components

### Sociable Unit Tests

```typescript
const { unit, unitRef } = await TestBed.sociable(UserService)
.expose(UserValidator) // Use real validator
.expose(UserRepository) // Use real repository
.compile();
```

Test multiple classes together with their real collaborators. External I/O (databases, APIs, file systems) is replaced with test doubles to keep tests fast.

**When to use:**

- Verifying components work together correctly
- Testing interactions between business logic components

**Trade-off:** Slower execution, multiple failure points

Both strategies are unit tests - they keep external I/O mocked and remain fast. Use both together for comprehensive coverage.

## What Each Example Demonstrates

- **Solitary unit tests** - Test one class in complete isolation with all dependencies mocked
- **Sociable unit tests** - Test multiple classes together with real collaborators, external I/O mocked
- **Type-safe mocking** - Full TypeScript support without manual setup
- **Zero boilerplate** - No test module configuration required

## Common Use Case

All examples implement the same user management service with three key components:

```mermaid
graph LR
UserService --> UserValidator
UserService --> UserRepository
UserRepository --> DATABASE_TOKEN
```

- **UserService** - Business logic layer with validation and persistence
- **UserValidator** - Email validation (no dependencies)
- **UserRepository** - Data access layer (depends on database token)

This consistent domain model makes it easy to compare different framework and test runner combinations.

## Repository Structure

```
examples/
├── nestjs-jest/ # NestJS with Jest
├── nestjs-vitest/ # NestJS with Vitest
├── nestjs-sinon/ # NestJS with Sinon
├── inversify-jest/ # InversifyJS with Jest
├── inversify-vitest/ # InversifyJS with Vitest
├── inversify-sinon/ # InversifyJS with Sinon
└── advanced-mock-config/ # Advanced .mock().final() and .impl() patterns
```

Each example contains two directories:

**`src/`** - Application code being tested:

- `types.ts` - Domain types and interfaces
- `user.service.ts` - Business logic layer
- `user.validator.ts` - Validation logic
- `user.repository.ts` - Data access layer

**`tests/`** - Tests demonstrating Suites usage:

- `user.solitary.spec.ts` - Solitary unit tests (all dependencies mocked)
- `user.sociable.spec.ts` - Sociable unit tests (real collaborators, external I/O mocked)

## Prerequisites

- Node.js 18 or higher
- pnpm installed globally
- Basic understanding of TypeScript and dependency injection

## Troubleshooting

### Tests fail after install

1. Check Node.js version: `node --version` (requires 18+)
2. Check pnpm: `pnpm --version`
3. Clear and reinstall: `rm -rf node_modules && pnpm install`
4. Verify working directory is the example directory, not repository root

### "reflect-metadata" errors (InversifyJS examples)

InversifyJS requires decorator metadata. Configuration is already set in `tsconfig.json` and imports. If errors occur, verify:

- `experimentalDecorators: true` in tsconfig.json
- `emitDecoratorMetadata: true` in tsconfig.json
- `import 'reflect-metadata'` at top of test files

### Sinon tests show different output format

Sinon uses Mocha test runner, which formats output differently than Jest/Vitest. All examples show 6 passing tests.

### "Module not found" errors

Run `pnpm install` in the specific example directory. Each example has standalone dependencies.

## Learn More

- [Suites Documentation](https://suites.dev)
- [Testing Strategies Guide](https://suites.dev/docs/testing-strategies)
- [NestJS Integration](https://suites.dev/docs/nestjs)
- [InversifyJS Integration](https://suites.dev/docs/inversify)
129 changes: 129 additions & 0 deletions advanced-mock-config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Suites + NestJS + Jest (Advanced Mock Configuration)

Simple user management example demonstrating [Suites](https://suites.dev) with NestJS and Jest, showcasing advanced mock configuration patterns using `.mock().final()` and `.mock().impl()`.

## Prerequisites

- Node.js 18 or higher
- pnpm installed globally

## What This Demonstrates

- ✅ **Solitary unit tests** - Test UserService in complete isolation
- ✅ **Sociable unit tests** - Test components together with real validation, mocked I/O
- ✅ **`.mock().final()`** - Immutable mock configuration with plain functions
- ✅ **`.mock().impl()`** - Flexible mock configuration with stub functions
- ✅ **Token injection** - DATABASE_TOKEN as external boundary
- ✅ **Class injection** - UserValidator and UserRepository

## Running the Example

```bash
pnpm install
pnpm test
```

All tests should pass, demonstrating both testing strategies with advanced mock configuration.

## Project Structure

**`src/`** - Application code being tested:

```
src/
├── types.ts # User types and interfaces
├── user.validator.ts # Validation logic (no dependencies)
├── user.repository.ts # Data access (token injection)
└── user.service.ts # Business logic (class injections)
```

**`tests/`** - Tests demonstrating Suites advanced mock configuration:

```
tests/
├── user.solitary.spec.ts # Solitary tests with .mock().final() and .mock().impl()
└── user.sociable.spec.ts # Sociable tests with .mock().final() and .mock().impl()
```

## Mock Configuration Patterns

### `.mock().final()` - Immutable Configuration

Use when you want to **lock down** mock behavior that should never change:

```typescript
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock(UserValidator)
.final({
// Plain functions - cannot be reconfigured in tests
validate: () => ({ isValid: true, errors: [] })
})
.compile();
```

**Key characteristics:**

- Functions provided to `.final()` are plain functions, not Jest mocks
- Behavior is locked - tests cannot use `mockReturnValue()` or similar
- Call inspection (`toHaveBeenCalled()`) is not available
- Best for: external services, logging, fixed configuration values

### `.mock().impl()` - Flexible Configuration

Use when you want **sensible defaults** that tests can override:

```typescript
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock(UserValidator)
.impl((stubFn) => ({
// Stubs - can be reconfigured and inspected in tests
validate: stubFn().mockReturnValue({ isValid: true, errors: [] })
}))
.compile();

// Later in tests, you can override:
validator.validate.mockReturnValue({ isValid: false, errors: ['Error'] });
```

**Key characteristics:**

- Uses `stubFn()` factory to create Jest mock functions
- Behavior is flexible - tests can reconfigure using `mockReturnValue()`, etc.
- Call inspection (`toHaveBeenCalled()`, `toHaveBeenCalledWith()`) is available
- Best for: most mocks where different tests need different behaviors

## Comparing Testing Strategies

**When to use `.final()`:**

- External APIs (email, SMS, payments) - prevent accidental real calls
- Configuration/settings - fixed test environment values
- Logging/telemetry - consistent, predictable output

**When to use `.impl()`:**

- Database operations - need to simulate different query results
- Collaborator services - need flexibility for different test scenarios
- Any mock where behavior needs to vary per-test

## Comparison Table

| Feature | `.final()` | `.impl()` |
| ---------------------- | ---------------- | ----------------- |
| Reconfigurable | ❌ No | ✅ Yes |
| Call inspection | ❌ No | ✅ Yes |
| Function type | Plain functions | Jest stubs |
| `mockReturnValue()` | ❌ Cannot use | ✅ Can use |
| `toHaveBeenCalled()` | ❌ Cannot use | ✅ Can use |

## Related Examples

- [nestjs-jest](../nestjs-jest) - Basic NestJS + Jest example
- [nestjs-vitest](../nestjs-vitest) - NestJS with Vitest
- [inversify-jest](../inversify-jest) - InversifyJS with Jest

## Learn More

- [Suites Documentation](https://suites.dev)
- [Mock Configuration API](https://suites.dev/docs/api-reference/mock-configuration)
- [Test Doubles Guide](https://suites.dev/docs/guides/test-doubles)
2 changes: 2 additions & 0 deletions advanced-mock-config/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="@suites/doubles.jest/unit" />

12 changes: 12 additions & 0 deletions advanced-mock-config/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
testEnvironment: 'node',
testRegex: 'tests/.*\\.spec\\.ts$',
transform: {
'^.+\\.ts$': ['ts-jest', { isolatedModules: true }]
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/types.ts'
]
};

25 changes: 25 additions & 0 deletions advanced-mock-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "advanced-mock-config-example",
"version": "0.0.0",
"private": true,
"description": "Suites Advanced Mock Configuration Example - .mock().final() and .mock().impl()",
"scripts": {
"test": "tsc --noEmit && jest"
},
"dependencies": {
"@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@suites/di.nestjs": "^3.0.1",
"@suites/doubles.jest": "^3.0.1",
"@suites/unit": "^3.0.1",
"@types/jest": "^29.5.13",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
},
"packageManager": "[email protected]+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}

Loading