Quick Start Guide 🚀
This guide will walk you through building a small application and testing it with Suites in just a few minutes. You'll see firsthand how Suites simplifies testing in dependency injection environments.
- Setting up a project with Suites
- Creating a simple application with dependency injection
- Writing your first unit test using Suites' TestBed
- Understanding key Suites concepts and patterns
Complete source code for this example (and more) is available in the Suites Examples repository.
Prerequisites
- Basic knowledge of TypeScript and unit testing
- Node.js installed on your system
- Familiarity with dependency injection concepts
Project Setup 🛠️
Let's create a new project and install the necessary dependencies:
1. Initialize Your Project
mkdir suites-quickstart
cd suites-quickstart
npm init -y
2. Install Suites Packages
# Install NestJS dependencies
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
# Install Suites core and adapters
npm install --save-dev @suites/unit @suites/di.nestjs @suites/doubles.jest
# Install TypeScript and Jest
npm install --save-dev ts-jest @types/jest jest typescript
3. Configure TypeScript and Jest
Create a tsconfig.json
file:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"noEmit": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"rootDir": "src",
"types": ["node", "jest"]
},
"include": [
"src/**/*.spec.ts",
"global.d.ts"
]
}
Configure Jest for TypeScript:
module.exports = {
testEnvironment: 'node',
testRegex: '.spec.ts$',
transform: { '.ts': ['ts-jest', { isolatedModules: true } ] },
};
Create a global.d.ts
file in your project root:
/// <reference types="@suites/doubles.jest/unit.d.ts" />
Creating a Simple Application 📝
Let's create a small application with two classes that demonstrate dependency injection:
1. Define Data Types
export interface User {
id: number;
name: string;
email: string;
}
2. Create a Repository Class
import { Injectable } from '@nestjs/common'; // Or your preferred DI framework
import { User } from './types';
@Injectable()
export class UserRepository {
async getUserById(id: number): Promise<User> {
// In a real app, this would fetch from a database
return { id, name: 'John Doe', email: 'john@example.com' };
}
}
3. Create a Service Class with a Dependency
import { Injectable } from '@nestjs/common'; // Or your preferred DI framework
import { UserRepository } from './user.repository';
import { User } from './types';
@Injectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
async getUserName(id: number): Promise<string> {
const user = await this.userRepository.getUserById(id);
return user.name;
}
}
Writing Your First Test with Suites ✅
Now let's write a test for our UserService
using Suites' TestBed:
import { type Mocked, TestBed } from '@suites/unit';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
describe('User Service Unit Spec', () => {
let userService: UserService;
let userRepository: Mocked<UserRepository>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(UserService).compile();
userService = unit;
userRepository = unitRef.get(UserRepository);
});
it('should return the user name and call repository', async () => {
userRepository.getUserById.mockResolvedValue({
id: 1,
name: 'John Doe',
email: 'john@doe.com'
});
const result = await userService.getUserName(1);
expect(userRepository.getUserById).toHaveBeenCalledWith(1);
expect(result).toBe('John Doe');
});
});
Running Your Test
Run your tests with:
npm test
Understanding How It Works 💡
Let's break down what happened in our test:
Key Components of Suites
- TestBed: A factory for creating isolated test environments
- Mocked<T>: A type that represents a mocked version of a class
- unitRef: An object that provides access to mocked dependencies
The Magic Behind the Scenes ✨
When you called TestBed.solitary(UserService).compile()
, Suites automatically:
- Created an instance of
UserService
for testing - Created a mock for
UserRepository
(its dependency) - Injected the mock into the
UserService
instance - Provided type-safe access to the mock through
unitRef
All of this happened without you needing to manually create mocks or configure dependency injection!
The Virtual DI Container ⚡
A key innovation in Suites is its "virtual DI container" approach. Here's what that means:
- No Full Container Initialization: Suites bypasses loading the entire DI container, which can be slow and bring in unrelated dependencies.
- Metadata-Driven: Instead, Suites uses the DI framework's own reflection and metadata capabilities to understand dependencies.
- Lightweight Construction: Suites builds a minimal, virtual container that includes only what's needed for your test.
- Framework Integration: This works seamlessly with your existing DI framework's annotations and decorators.
In traditional testing, you might:
- Initialize the full DI container
- Register mock implementations for dependencies
- Retrieve the service under test
With Suites' virtual DI container, you get:
- Faster test execution (no framework overhead)
- Only the dependencies you need (reduced complexity)
- The same dependency resolution rules as your actual application
- Automatic mock creation for dependencies
This approach, inspired by Martin Fowler's writings on test isolation, gives you the benefits of dependency injection without the performance and complexity costs.
Beyond the Basics
Suites offers many more capabilities to streamline your testing workflow:
Configuring Mock Behavior
// Using .mock().final() for immutable responses
const { unit } = await TestBed.solitary(UserService)
.mock(UserRepository)
.final({
getUserById: async () => ({ id: 1, name: 'John Doe', email: 'john@example.com' })
})
.compile();
// Using .mock().impl() for flexible responses
const { unit, unitRef } = await TestBed.solitary(UserService)
.mock(UserRepository)
.impl(stubFn => ({
getUserById: stubFn().mockResolvedValue({ id: 1, name: 'John Doe', email: 'john@example.com' })
}))
.compile();