Skip to content

Commit d70c02e

Browse files
authored
Merge pull request #2 from suites-dev/add-ci-workflow
refactor: setup all examples and ci
2 parents 27304bf + 2f59d74 commit d70c02e

File tree

87 files changed

+14158
-527
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+14158
-527
lines changed

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Continuous Integration
2+
3+
on:
4+
push:
5+
branches: main
6+
pull_request:
7+
branches: main
8+
9+
jobs:
10+
test:
11+
name: Test ${{ matrix.example }} (Node ${{ matrix.node-version }})
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 10
14+
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
example:
19+
- advanced-mock-config
20+
- inversify-jest
21+
- inversify-sinon
22+
- inversify-vitest
23+
- nestjs-jest
24+
- nestjs-sinon
25+
- nestjs-vitest
26+
node-version:
27+
- '18'
28+
- '20'
29+
- '22'
30+
31+
steps:
32+
- name: Checkout repository
33+
uses: actions/checkout@v4
34+
35+
- name: Setup pnpm
36+
uses: pnpm/action-setup@v2
37+
with:
38+
version: 9.15.4
39+
40+
- name: Setup Node.js
41+
uses: actions/setup-node@v4
42+
with:
43+
node-version: ${{ matrix.node-version }}
44+
cache: 'pnpm'
45+
cache-dependency-path: ${{ matrix.example }}/pnpm-lock.yaml
46+
47+
- name: Cache node_modules
48+
uses: actions/cache@v4
49+
with:
50+
path: ${{ matrix.example }}/node_modules
51+
key: modules-${{ runner.os }}-node${{ matrix.node-version }}-${{ matrix.example }}-${{ hashFiles(format('{0}/pnpm-lock.yaml', matrix.example)) }}
52+
restore-keys: |
53+
modules-${{ runner.os }}-node${{ matrix.node-version }}-${{ matrix.example }}-
54+
55+
- name: Install dependencies
56+
working-directory: ${{ matrix.example }}
57+
run: pnpm install --frozen-lockfile
58+
59+
- name: Run tests
60+
working-directory: ${{ matrix.example }}
61+
run: pnpm test
62+
63+
- name: Report results
64+
if: always()
65+
run: |
66+
echo "### ${{ matrix.example }} (Node ${{ matrix.node-version }})" >> $GITHUB_STEP_SUMMARY
67+
echo "Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY

README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Suites Examples
2+
3+
Real-world examples demonstrating [Suites](https://suites.dev) integration with popular dependency injection frameworks and test runners.
4+
5+
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.
6+
7+
If you are new to Suites, check out the [Getting Started](https://suites.dev/docs/getting-started) guide.
8+
9+
## Examples
10+
11+
| Example | DI Framework | Test Runner | Use When |
12+
| ---------------------------------------------- | ------------ | ----------- | ------------------------------------------------- |
13+
| [nestjs-jest](./nestjs-jest) | NestJS | Jest | NestJS with Jest |
14+
| [nestjs-vitest](./nestjs-vitest) | NestJS | Vitest | NestJS with Vitest |
15+
| [nestjs-sinon](./nestjs-sinon) | NestJS | Sinon | NestJS with Sinon/Mocha |
16+
| [inversify-jest](./inversify-jest) | InversifyJS | Jest | InversifyJS with Jest |
17+
| [inversify-vitest](./inversify-vitest) | InversifyJS | Vitest | InversifyJS with Vitest |
18+
| [inversify-sinon](./inversify-sinon) | InversifyJS | Sinon | InversifyJS with Sinon/Mocha |
19+
| [advanced-mock-config](./advanced-mock-config) | NestJS | Jest | Advanced `.mock().final()` and `.impl()` patterns |
20+
21+
## Quick Start
22+
23+
```bash
24+
# Clone and run any example
25+
cd nestjs-jest
26+
pnpm install
27+
pnpm test
28+
```
29+
30+
All tests should pass immediately, demonstrating both testing strategies.
31+
32+
## Testing Strategies
33+
34+
Each example demonstrates two approaches:
35+
36+
### Solitary Unit Tests
37+
38+
```typescript
39+
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
40+
```
41+
42+
Test one class in complete isolation. All dependencies are replaced with test doubles.
43+
44+
**When to use:**
45+
46+
- Testing component logic in isolation
47+
- Controlling all inputs for predictable results
48+
49+
**Trade-off:** Does not verify interactions between components
50+
51+
### Sociable Unit Tests
52+
53+
```typescript
54+
const { unit, unitRef } = await TestBed.sociable(UserService)
55+
.expose(UserValidator) // Use real validator
56+
.expose(UserRepository) // Use real repository
57+
.compile();
58+
```
59+
60+
Test multiple classes together with their real collaborators. External I/O (databases, APIs, file systems) is replaced with test doubles to keep tests fast.
61+
62+
**When to use:**
63+
64+
- Verifying components work together correctly
65+
- Testing interactions between business logic components
66+
67+
**Trade-off:** Slower execution, multiple failure points
68+
69+
Both strategies are unit tests - they keep external I/O mocked and remain fast. Use both together for comprehensive coverage.
70+
71+
## What Each Example Demonstrates
72+
73+
- **Solitary unit tests** - Test one class in complete isolation with all dependencies mocked
74+
- **Sociable unit tests** - Test multiple classes together with real collaborators, external I/O mocked
75+
- **Type-safe mocking** - Full TypeScript support without manual setup
76+
- **Zero boilerplate** - No test module configuration required
77+
78+
## Common Use Case
79+
80+
All examples implement the same user management service with three key components:
81+
82+
```mermaid
83+
graph LR
84+
UserService --> UserValidator
85+
UserService --> UserRepository
86+
UserRepository --> DATABASE_TOKEN
87+
```
88+
89+
- **UserService** - Business logic layer with validation and persistence
90+
- **UserValidator** - Email validation (no dependencies)
91+
- **UserRepository** - Data access layer (depends on database token)
92+
93+
This consistent domain model makes it easy to compare different framework and test runner combinations.
94+
95+
## Repository Structure
96+
97+
```
98+
examples/
99+
├── nestjs-jest/ # NestJS with Jest
100+
├── nestjs-vitest/ # NestJS with Vitest
101+
├── nestjs-sinon/ # NestJS with Sinon
102+
├── inversify-jest/ # InversifyJS with Jest
103+
├── inversify-vitest/ # InversifyJS with Vitest
104+
├── inversify-sinon/ # InversifyJS with Sinon
105+
└── advanced-mock-config/ # Advanced .mock().final() and .impl() patterns
106+
```
107+
108+
Each example contains two directories:
109+
110+
**`src/`** - Application code being tested:
111+
112+
- `types.ts` - Domain types and interfaces
113+
- `user.service.ts` - Business logic layer
114+
- `user.validator.ts` - Validation logic
115+
- `user.repository.ts` - Data access layer
116+
117+
**`tests/`** - Tests demonstrating Suites usage:
118+
119+
- `user.solitary.spec.ts` - Solitary unit tests (all dependencies mocked)
120+
- `user.sociable.spec.ts` - Sociable unit tests (real collaborators, external I/O mocked)
121+
122+
## Prerequisites
123+
124+
- Node.js 18 or higher
125+
- pnpm installed globally
126+
- Basic understanding of TypeScript and dependency injection
127+
128+
## Troubleshooting
129+
130+
### Tests fail after install
131+
132+
1. Check Node.js version: `node --version` (requires 18+)
133+
2. Check pnpm: `pnpm --version`
134+
3. Clear and reinstall: `rm -rf node_modules && pnpm install`
135+
4. Verify working directory is the example directory, not repository root
136+
137+
### "reflect-metadata" errors (InversifyJS examples)
138+
139+
InversifyJS requires decorator metadata. Configuration is already set in `tsconfig.json` and imports. If errors occur, verify:
140+
141+
- `experimentalDecorators: true` in tsconfig.json
142+
- `emitDecoratorMetadata: true` in tsconfig.json
143+
- `import 'reflect-metadata'` at top of test files
144+
145+
### Sinon tests show different output format
146+
147+
Sinon uses Mocha test runner, which formats output differently than Jest/Vitest. All examples show 6 passing tests.
148+
149+
### "Module not found" errors
150+
151+
Run `pnpm install` in the specific example directory. Each example has standalone dependencies.
152+
153+
## Learn More
154+
155+
- [Suites Documentation](https://suites.dev)
156+
- [Testing Strategies Guide](https://suites.dev/docs/testing-strategies)
157+
- [NestJS Integration](https://suites.dev/docs/nestjs)
158+
- [InversifyJS Integration](https://suites.dev/docs/inversify)

advanced-mock-config/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Suites + NestJS + Jest (Advanced Mock Configuration)
2+
3+
Simple user management example demonstrating [Suites](https://suites.dev) with NestJS and Jest, showcasing advanced mock configuration patterns using `.mock().final()` and `.mock().impl()`.
4+
5+
## Prerequisites
6+
7+
- Node.js 18 or higher
8+
- pnpm installed globally
9+
10+
## What This Demonstrates
11+
12+
-**Solitary unit tests** - Test UserService in complete isolation
13+
-**Sociable unit tests** - Test components together with real validation, mocked I/O
14+
-**`.mock().final()`** - Immutable mock configuration with plain functions
15+
-**`.mock().impl()`** - Flexible mock configuration with stub functions
16+
-**Token injection** - DATABASE_TOKEN as external boundary
17+
-**Class injection** - UserValidator and UserRepository
18+
19+
## Running the Example
20+
21+
```bash
22+
pnpm install
23+
pnpm test
24+
```
25+
26+
All tests should pass, demonstrating both testing strategies with advanced mock configuration.
27+
28+
## Project Structure
29+
30+
**`src/`** - Application code being tested:
31+
32+
```
33+
src/
34+
├── types.ts # User types and interfaces
35+
├── user.validator.ts # Validation logic (no dependencies)
36+
├── user.repository.ts # Data access (token injection)
37+
└── user.service.ts # Business logic (class injections)
38+
```
39+
40+
**`tests/`** - Tests demonstrating Suites advanced mock configuration:
41+
42+
```
43+
tests/
44+
├── user.solitary.spec.ts # Solitary tests with .mock().final() and .mock().impl()
45+
└── user.sociable.spec.ts # Sociable tests with .mock().final() and .mock().impl()
46+
```
47+
48+
## Mock Configuration Patterns
49+
50+
### `.mock().final()` - Immutable Configuration
51+
52+
Use when you want to **lock down** mock behavior that should never change:
53+
54+
```typescript
55+
const { unit, unitRef } = await TestBed.solitary(UserService)
56+
.mock(UserValidator)
57+
.final({
58+
// Plain functions - cannot be reconfigured in tests
59+
validate: () => ({ isValid: true, errors: [] })
60+
})
61+
.compile();
62+
```
63+
64+
**Key characteristics:**
65+
66+
- Functions provided to `.final()` are plain functions, not Jest mocks
67+
- Behavior is locked - tests cannot use `mockReturnValue()` or similar
68+
- Call inspection (`toHaveBeenCalled()`) is not available
69+
- Best for: external services, logging, fixed configuration values
70+
71+
### `.mock().impl()` - Flexible Configuration
72+
73+
Use when you want **sensible defaults** that tests can override:
74+
75+
```typescript
76+
const { unit, unitRef } = await TestBed.solitary(UserService)
77+
.mock(UserValidator)
78+
.impl((stubFn) => ({
79+
// Stubs - can be reconfigured and inspected in tests
80+
validate: stubFn().mockReturnValue({ isValid: true, errors: [] })
81+
}))
82+
.compile();
83+
84+
// Later in tests, you can override:
85+
validator.validate.mockReturnValue({ isValid: false, errors: ['Error'] });
86+
```
87+
88+
**Key characteristics:**
89+
90+
- Uses `stubFn()` factory to create Jest mock functions
91+
- Behavior is flexible - tests can reconfigure using `mockReturnValue()`, etc.
92+
- Call inspection (`toHaveBeenCalled()`, `toHaveBeenCalledWith()`) is available
93+
- Best for: most mocks where different tests need different behaviors
94+
95+
## Comparing Testing Strategies
96+
97+
**When to use `.final()`:**
98+
99+
- External APIs (email, SMS, payments) - prevent accidental real calls
100+
- Configuration/settings - fixed test environment values
101+
- Logging/telemetry - consistent, predictable output
102+
103+
**When to use `.impl()`:**
104+
105+
- Database operations - need to simulate different query results
106+
- Collaborator services - need flexibility for different test scenarios
107+
- Any mock where behavior needs to vary per-test
108+
109+
## Comparison Table
110+
111+
| Feature | `.final()` | `.impl()` |
112+
| ---------------------- | ---------------- | ----------------- |
113+
| Reconfigurable | ❌ No | ✅ Yes |
114+
| Call inspection | ❌ No | ✅ Yes |
115+
| Function type | Plain functions | Jest stubs |
116+
| `mockReturnValue()` | ❌ Cannot use | ✅ Can use |
117+
| `toHaveBeenCalled()` | ❌ Cannot use | ✅ Can use |
118+
119+
## Related Examples
120+
121+
- [nestjs-jest](../nestjs-jest) - Basic NestJS + Jest example
122+
- [nestjs-vitest](../nestjs-vitest) - NestJS with Vitest
123+
- [inversify-jest](../inversify-jest) - InversifyJS with Jest
124+
125+
## Learn More
126+
127+
- [Suites Documentation](https://suites.dev)
128+
- [Mock Configuration API](https://suites.dev/docs/api-reference/mock-configuration)
129+
- [Test Doubles Guide](https://suites.dev/docs/guides/test-doubles)

advanced-mock-config/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="@suites/doubles.jest/unit" />
2+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testRegex: 'tests/.*\\.spec\\.ts$',
4+
transform: {
5+
'^.+\\.ts$': ['ts-jest', { isolatedModules: true }]
6+
},
7+
collectCoverageFrom: [
8+
'src/**/*.ts',
9+
'!src/types.ts'
10+
]
11+
};
12+

advanced-mock-config/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "advanced-mock-config-example",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Suites Advanced Mock Configuration Example - .mock().final() and .mock().impl()",
6+
"scripts": {
7+
"test": "tsc --noEmit && jest"
8+
},
9+
"dependencies": {
10+
"@nestjs/common": "^10.4.15",
11+
"@nestjs/core": "^10.4.15",
12+
"reflect-metadata": "^0.2.2"
13+
},
14+
"devDependencies": {
15+
"@suites/di.nestjs": "^3.0.1",
16+
"@suites/doubles.jest": "^3.0.1",
17+
"@suites/unit": "^3.0.1",
18+
"@types/jest": "^29.5.13",
19+
"jest": "^29.7.0",
20+
"ts-jest": "^29.2.5",
21+
"typescript": "^5.7.2"
22+
},
23+
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
24+
}
25+

0 commit comments

Comments
 (0)