diff --git a/docs/recipes/index.md b/docs/recipes/index.md new file mode 100644 index 0000000..f6f0018 --- /dev/null +++ b/docs/recipes/index.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 3 +title: Recipes +description: Real-world examples and patterns for testing with Suites +--- + +# Recipes + +> **What this covers:** Real-world examples for testing with Suites across various technologies \ +> **Best for:** Developers looking for practical patterns beyond core testing concepts + +This section contains real-world examples demonstrating how to use Suites with various technologies and frameworks. These examples go beyond the core testing concepts and show practical patterns for common scenarios. + +## Examples Repository + +For complete, runnable code examples, browse the **[Suites Examples repository](https://github.com/suites-dev/examples)**. The repository contains: + +- Working implementations of all patterns shown in these guides +- Complete test suites for each example +- Different framework integrations (NestJS, InversifyJS) +- Real-world scenarios and edge cases + +## Available Examples + +- **[Mocking ORMs](/docs/recipes/mocking-orm)** - Learn how to mock TypeORM, Prisma, Drizzle, and MikroORM in your unit tests + +## Contributing Examples + +Have a pattern or example you'd like to share? Contributions are welcome! You can: + +1. Submit examples to the [Examples repository](https://github.com/suites-dev/examples) +2. Suggest documentation improvements via issues or pull requests diff --git a/docs/recipes/mocking-orm/drizzle.md b/docs/recipes/mocking-orm/drizzle.md new file mode 100644 index 0000000..5476b6c --- /dev/null +++ b/docs/recipes/mocking-orm/drizzle.md @@ -0,0 +1,223 @@ +--- +sidebar_position: 11 +title: Mocking Drizzle +description: How to mock Drizzle database instances in your unit tests +--- + +# Mocking Drizzle + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +:::tip Complete Examples +For complete, runnable Drizzle examples, see the [Drizzle examples](https://github.com/suites-dev/examples/tree/main/nestjs-jest-drizzle) in the Suites Examples repository. +::: + +Drizzle uses a database instance that you typically import directly. Wrap it in an injectable class. + +## Step 1: Create a Database Injectable + +```typescript title="database.service.ts" +import { Injectable } from "@nestjs/common"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as schema from "./schema"; + +@Injectable() +export class DatabaseService { + private db: ReturnType; + + constructor() { + const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + }); + this.db = drizzle(pool, { schema }); + } + + getDb() { + return this.db; + } +} +``` + +## Step 2: Create a Repository Wrapper + +```typescript title="user.repository.ts" +import { Injectable } from "@nestjs/common"; +import { DatabaseService } from "./database.service"; +import { users } from "./schema"; +import { eq } from "drizzle-orm"; + +@Injectable() +export class UserRepository { + constructor(private readonly database: DatabaseService) {} + + async findById(id: number) { + const db = this.database.getDb(); + const result = await db + .select() + .from(users) + .where(eq(users.id, id)) + .limit(1); + return result[0] || null; + } + + async findByEmail(email: string) { + const db = this.database.getDb(); + const result = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + return result[0] || null; + } + + async create(email: string, name: string) { + const db = this.database.getDb(); + const result = await db.insert(users).values({ email, name }).returning(); + return result[0]; + } +} +``` + +## Step 3: Use the Repository in Your Service + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "./user.repository"; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async getUserById(id: number) { + return this.userRepository.findById(id); + } + + async createUser(email: string, name: string) { + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) { + throw new Error("User already exists"); + } + + return this.userRepository.create(email, name); + } +} +``` + +## Step 4: Test with Suites + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { UserRepository } from "./user.repository"; + +describe("UserService", () => { + let userService: UserService; + let userRepository: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + userRepository = unitRef.get(UserRepository); + }); + + it("should get user by id", async () => { + const mockUser = { id: 1, email: "test@example.com", name: "Test User" }; + userRepository.findById.mockResolvedValue(mockUser); + + const result = await userService.getUserById(1); + + expect(result).toEqual(mockUser); + expect(userRepository.findById).toHaveBeenCalledWith(1); + }); + + it("should create a new user", async () => { + userRepository.findByEmail.mockResolvedValue(null); + const newUser = { id: 1, email: "new@example.com", name: "New User" }; + userRepository.create.mockResolvedValue(newUser); + + const result = await userService.createUser("new@example.com", "New User"); + + expect(result).toEqual(newUser); + expect(userRepository.findByEmail).toHaveBeenCalledWith("new@example.com"); + expect(userRepository.create).toHaveBeenCalledWith( + "new@example.com", + "New User" + ); + }); +}); +``` + +## Direct Database Injection + +If you prefer to inject DatabaseService directly and mock the database instance: + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { DatabaseService } from "./database.service"; +import { users } from "./schema"; +import { eq } from "drizzle-orm"; + +@Injectable() +export class UserService { + constructor(private readonly database: DatabaseService) {} + + async getUserById(id: number) { + const db = this.database.getDb(); + const result = await db + .select() + .from(users) + .where(eq(users.id, id)) + .limit(1); + return result[0] || null; + } +} +``` + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { DatabaseService } from "./database.service"; + +describe("UserService", () => { + let userService: UserService; + let database: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + database = unitRef.get(DatabaseService); + }); + + it("should get user by id", async () => { + const mockDb = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + limit: jest + .fn() + .mockResolvedValue([{ id: 1, email: "test@example.com" }]), + }; + database.getDb.mockReturnValue(mockDb as any); + + const result = await userService.getUserById(1); + + expect(result).toEqual({ id: 1, email: "test@example.com" }); + expect(database.getDb).toHaveBeenCalled(); + }); +}); +``` + +## Summary + +- **Wrap Drizzle database instance** in an injectable `DatabaseService` class to make it mockable +- **Create repository wrappers** for clean separation between data access and business logic +- **Use Suites** to automatically mock repository dependencies in your service tests +- **Direct database injection** is possible but requires chained mock setup + +## Next Steps + +- **[Solitary Unit Tests](/docs/guides/solitary)**: Deep dive into testing in isolation +- **[Test Doubles](/docs/guides/test-doubles)**: Understand mocks and stubs in depth diff --git a/docs/recipes/mocking-orm/index.md b/docs/recipes/mocking-orm/index.md new file mode 100644 index 0000000..41f8e59 --- /dev/null +++ b/docs/recipes/mocking-orm/index.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 8 +title: Mocking ORMs +description: How to mock TypeORM, Prisma, Drizzle, and MikroORM in your unit tests +--- + +# Mocking ORMs + +> **What this covers:** Mocking Object-Relational Mapping (ORM) libraries like TypeORM, Prisma, Drizzle, and MikroORM \ +> **Time to read:** ~12 minutes \ +> **Prerequisites:** [Unit Testing Fundamentals](/docs/guides/fundamentals), [Solitary Unit Tests](/docs/guides/solitary) \ +> **Best for:** Testing services that interact with databases without hitting real database connections + +When testing services that interact with databases, you need to mock ORM clients to keep tests isolated. This guide shows you how to structure your code and write tests for popular ORMs: TypeORM, Prisma, Drizzle, and MikroORM. + +## Overview + +This guide covers: + +1. The pattern: Wrapping ORM clients in injectables +2. [TypeORM](/docs/recipes/mocking-orm/typeorm): Mocking repositories and entity managers +3. [Prisma](/docs/recipes/mocking-orm/prisma): Mocking Prisma client instances +4. [Drizzle](/docs/recipes/mocking-orm/drizzle): Mocking Drizzle database instances +5. [MikroORM](/docs/recipes/mocking-orm/mikroorm): Mocking entity managers and repositories + +## The Pattern: Wrap ORM Clients with Injectables + +ORMs typically provide clients or managers that you import directly. To make them mockable with Suites, wrap them in injectable classes that your business logic depends on. + +**Why wrap ORM clients?** + +- **Explicit dependencies**: Suites can only mock dependencies passed through constructors +- **Type safety**: Full TypeScript support for mocked methods +- **Testability**: Easy to replace with mocks in tests +- **Abstraction**: Business logic doesn't depend on specific ORM implementation details + +## ORM-Specific Guides + +Each ORM has its own guide with detailed examples: + +- **[TypeORM](/docs/recipes/mocking-orm/typeorm)** - Mocking TypeORM repositories and EntityManager +- **[Prisma](/docs/recipes/mocking-orm/prisma)** - Mocking Prisma client instances +- **[Drizzle](/docs/recipes/mocking-orm/drizzle)** - Mocking Drizzle database instances +- **[MikroORM](/docs/recipes/mocking-orm/mikroorm)** - Mocking MikroORM EntityManager and repositories + +## Summary + +- **Wrap ORM clients** in injectables to make them mockable +- **Create repository classes** that encapsulate ORM-specific logic +- **Use Suites** to automatically mock repository dependencies +- **Keep repositories focused** on data access, not business logic +- **Type everything** for full TypeScript support +- **Test error scenarios** in addition to happy paths + +## Next Steps + +- **[Solitary Unit Tests](/docs/guides/solitary)**: Learn more about testing in isolation +- **[Sociable Unit Tests](/docs/guides/sociable)**: Test multiple components together +- **[Test Doubles](/docs/guides/test-doubles)**: Understand mocks and stubs in depth diff --git a/docs/recipes/mocking-orm/mikroorm.md b/docs/recipes/mocking-orm/mikroorm.md new file mode 100644 index 0000000..34deac8 --- /dev/null +++ b/docs/recipes/mocking-orm/mikroorm.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 12 +title: Mocking MikroORM +description: How to mock MikroORM entity managers and repositories in your unit tests +--- + +# Mocking MikroORM + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +:::tip Complete Examples +For complete, runnable MikroORM examples, see the [MikroORM examples](https://github.com/suites-dev/examples/tree/main/nestjs-jest-mikroorm) in the Suites Examples repository. +::: + +MikroORM uses entity managers and repositories to interact with the database. Wrap these in injectable classes. + +If you are using NestJS, you can follow the [NestJS MikroORM documentation](https://docs.nestjs.com/recipes/mikroorm). + +## Step 1: Create an Injectable Repository + +```typescript title="user.repository.ts" +import { Injectable } from "@nestjs/common"; +import { EntityManager } from "@mikro-orm/core"; +import { User } from "./user.entity"; + +@Injectable() +export class UserRepository { + constructor(private readonly em: EntityManager) {} + + async findById(id: number): Promise { + return this.em.findOne(User, { id }); + } + + async findByEmail(email: string): Promise { + return this.em.findOne(User, { email }); + } + + async create(email: string, name: string): Promise { + const user = this.em.create(User, { email, name }); + await this.em.persistAndFlush(user); + return user; + } + + async save(user: User): Promise { + await this.em.persistAndFlush(user); + return user; + } + + async delete(id: number): Promise { + const user = await this.em.findOne(User, { id }); + if (user) { + await this.em.removeAndFlush(user); + } + } +} +``` + +## Step 2: Use the Repository in Your Service + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "./user.repository"; +import { User } from "./user.entity"; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async getUserById(id: number): Promise { + return this.userRepository.findById(id); + } + + async createUser(email: string, name: string): Promise { + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) { + throw new Error("User already exists"); + } + + return this.userRepository.create(email, name); + } +} +``` + +## Step 3: Test with Suites + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { UserRepository } from "./user.repository"; +import { User } from "./user.entity"; + +describe("UserService", () => { + let userService: UserService; + let userRepository: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + userRepository = unitRef.get(UserRepository); + }); + + it("should get user by id", async () => { + const mockUser: User = { + id: 1, + email: "test@example.com", + name: "Test User", + }; + userRepository.findById.mockResolvedValue(mockUser); + + const result = await userService.getUserById(1); + + expect(result).toEqual(mockUser); + expect(userRepository.findById).toHaveBeenCalledWith(1); + }); + + it("should create a new user", async () => { + userRepository.findByEmail.mockResolvedValue(null); + const newUser: User = { id: 1, email: "new@example.com", name: "New User" }; + userRepository.create.mockResolvedValue(newUser); + + const result = await userService.createUser("new@example.com", "New User"); + + expect(result).toEqual(newUser); + expect(userRepository.findByEmail).toHaveBeenCalledWith("new@example.com"); + expect(userRepository.create).toHaveBeenCalledWith( + "new@example.com", + "New User" + ); + }); + + it("should throw error if user already exists", async () => { + const existingUser: User = { + id: 1, + email: "existing@example.com", + name: "Existing", + }; + userRepository.findByEmail.mockResolvedValue(existingUser); + + await expect( + userService.createUser("existing@example.com", "New Name") + ).rejects.toThrow("User already exists"); + + expect(userRepository.create).not.toHaveBeenCalled(); + }); +}); +``` + +## Using Entity Manager Directly + +If you need to use MikroORM's EntityManager directly for transactions or complex queries: + +```typescript title="transaction.service.ts" +import { Injectable } from "@nestjs/common"; +import { EntityManager } from "@mikro-orm/core"; + +@Injectable() +export class TransactionService { + constructor(private readonly em: EntityManager) {} + + async executeInTransaction( + callback: (em: EntityManager) => Promise + ): Promise { + return this.em.transactional(callback); + } +} +``` + +```typescript title="transaction.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { TransactionService } from "./transaction.service"; +import { EntityManager } from "@mikro-orm/core"; + +describe("TransactionService", () => { + let transactionService: TransactionService; + let entityManager: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary( + TransactionService + ).compile(); + transactionService = unit; + entityManager = unitRef.get(EntityManager); + }); + + it("should execute callback in transaction", async () => { + const callback = jest.fn().mockResolvedValue("result"); + entityManager.transactional.mockImplementation(async (fn) => + fn(entityManager) + ); + + const result = await transactionService.executeInTransaction(callback); + + expect(result).toBe("result"); + expect(entityManager.transactional).toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith(entityManager); + }); +}); +``` + +## Summary + +- **Wrap MikroORM EntityManager** in injectable repository classes to make them mockable +- **Use Suites** to automatically mock repository dependencies in your service tests +- **EntityManager** can be injected directly for transactions and complex queries +- **Keep repositories focused** on data access, separating concerns from business logic + +## Next Steps + +- **[Solitary Unit Tests](/docs/guides/solitary)**: Deep dive into testing in isolation +- **[Test Doubles](/docs/guides/test-doubles)**: Understand mocks and stubs in depth diff --git a/docs/recipes/mocking-orm/prisma.md b/docs/recipes/mocking-orm/prisma.md new file mode 100644 index 0000000..13fdbc1 --- /dev/null +++ b/docs/recipes/mocking-orm/prisma.md @@ -0,0 +1,213 @@ +--- +sidebar_position: 10 +title: Mocking Prisma +description: How to mock Prisma client instances in your unit tests +--- + +# Mocking Prisma + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +:::tip Complete Examples +For complete, runnable Prisma examples, see the [Prisma examples](https://github.com/suites-dev/examples/tree/main/nestjs-jest-prisma) in the Suites Examples repository. +::: + +Prisma uses a generated client that you typically import directly. Wrap it in an injectable class. + +If you are using NestJS, you can follow the [NestJS Prisma documentation](https://docs.nestjs.com/recipes/prisma). + +## Step 1: Create a Prisma Injectable + +```typescript title="prisma.service.ts" +import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} +``` + +## Step 2: Create a Repository Wrapper + +```typescript title="user.repository.ts" +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; +import { User, Prisma } from "@prisma/client"; + +@Injectable() +export class UserRepository { + constructor(private readonly prisma: PrismaService) {} + + async findById(id: number): Promise { + return this.prisma.user.findUnique({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.prisma.user.findUnique({ where: { email } }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return this.prisma.user.create({ data }); + } + + async update(id: number, data: Prisma.UserUpdateInput): Promise { + return this.prisma.user.update({ where: { id }, data }); + } + + async delete(id: number): Promise { + return this.prisma.user.delete({ where: { id } }); + } +} +``` + +## Step 3: Use the Repository in Your Service + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "./user.repository"; +import { User } from "@prisma/client"; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async getUserById(id: number): Promise { + return this.userRepository.findById(id); + } + + async createUser(email: string, name: string): Promise { + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) { + throw new Error("User already exists"); + } + + return this.userRepository.create({ email, name }); + } +} +``` + +## Step 4: Test with Suites + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { UserRepository } from "./user.repository"; +import { User } from "@prisma/client"; + +describe("UserService", () => { + let userService: UserService; + let userRepository: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + userRepository = unitRef.get(UserRepository); + }); + + it("should get user by id", async () => { + const mockUser: User = { + id: 1, + email: "test@example.com", + name: "Test User", + createdAt: new Date(), + updatedAt: new Date(), + }; + userRepository.findById.mockResolvedValue(mockUser); + + const result = await userService.getUserById(1); + + expect(result).toEqual(mockUser); + expect(userRepository.findById).toHaveBeenCalledWith(1); + }); + + it("should create a new user", async () => { + userRepository.findByEmail.mockResolvedValue(null); + const newUser: User = { + id: 1, + email: "new@example.com", + name: "New User", + createdAt: new Date(), + updatedAt: new Date(), + }; + userRepository.create.mockResolvedValue(newUser); + + const result = await userService.createUser("new@example.com", "New User"); + + expect(result).toEqual(newUser); + expect(userRepository.findByEmail).toHaveBeenCalledWith("new@example.com"); + expect(userRepository.create).toHaveBeenCalledWith({ + email: "new@example.com", + name: "New User", + }); + }); +}); +``` + +## Direct Prisma Client Injection + +If you prefer to inject PrismaService directly: + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; + +@Injectable() +export class UserService { + constructor(private readonly prisma: PrismaService) {} + + async getUserById(id: number) { + return this.prisma.user.findUnique({ where: { id } }); + } +} +``` + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { PrismaService } from "./prisma.service"; + +describe("UserService", () => { + let userService: UserService; + let prisma: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + prisma = unitRef.get(PrismaService); + }); + + it("should get user by id", async () => { + const mockUser = { id: 1, email: "test@example.com", name: "Test" }; + prisma.user.findUnique.mockResolvedValue(mockUser as any); + + const result = await userService.getUserById(1); + + expect(result).toEqual(mockUser); + expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { id: 1 } }); + }); +}); +``` + +## Summary + +- **Wrap Prisma client** in an injectable `PrismaService` class to make it mockable +- **Create repository wrappers** for clean separation between data access and business logic +- **Use Suites** to automatically mock repository dependencies in your service tests +- **Direct client injection** is possible but requires more complex mock setup + +## Next Steps + +- **[Solitary Unit Tests](/docs/guides/solitary)**: Deep dive into testing in isolation +- **[Test Doubles](/docs/guides/test-doubles)**: Understand mocks and stubs in depth diff --git a/docs/recipes/mocking-orm/typeorm.md b/docs/recipes/mocking-orm/typeorm.md new file mode 100644 index 0000000..7be5305 --- /dev/null +++ b/docs/recipes/mocking-orm/typeorm.md @@ -0,0 +1,206 @@ +--- +sidebar_position: 9 +title: Mocking TypeORM +description: How to mock TypeORM repositories and entity managers in your unit tests +--- + +# Mocking TypeORM + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +:::tip Complete Examples +For complete, runnable TypeORM examples, see the [TypeORM examples](https://github.com/suites-dev/examples/tree/main/nestjs-jest-typeorm) in the Suites Examples repository. +::: + +TypeORM uses repositories and entity managers to interact with the database. Wrap these in injectables. + +If you are using NestJS, you can follow the [NestJS TypeORM documentation](https://docs.nestjs.com/recipes/sql-typeorm). + +## Step 1: Create an Injectable Repository + +```typescript title="user.repository.ts" +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { User } from "./user.entity"; + +@Injectable() +export class UserRepository { + constructor( + @InjectRepository(User) + private readonly typeOrmRepo: Repository + ) {} + + async findById(id: number): Promise { + return this.typeOrmRepo.findOne({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.typeOrmRepo.findOne({ where: { email } }); + } + + async save(user: User): Promise { + return this.typeOrmRepo.save(user); + } + + async delete(id: number): Promise { + await this.typeOrmRepo.delete(id); + } +} +``` + +## Step 2: Use the Repository in Your Service + +```typescript title="user.service.ts" +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "./user.repository"; +import { User } from "./user.entity"; + +@Injectable() +export class UserService { + constructor(private readonly userRepository: UserRepository) {} + + async getUserById(id: number): Promise { + return this.userRepository.findById(id); + } + + async createUser(email: string, name: string): Promise { + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) { + throw new Error("User already exists"); + } + + const user = new User(); + user.email = email; + user.name = name; + return this.userRepository.save(user); + } +} +``` + +## Step 3: Test with Suites + +```typescript title="user.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { UserService } from "./user.service"; +import { UserRepository } from "./user.repository"; +import { User } from "./user.entity"; + +describe("UserService", () => { + let userService: UserService; + let userRepository: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary(UserService).compile(); + userService = unit; + userRepository = unitRef.get(UserRepository); + }); + + it("should get user by id", async () => { + const mockUser: User = { + id: 1, + email: "test@example.com", + name: "Test User", + }; + userRepository.findById.mockResolvedValue(mockUser); + + const result = await userService.getUserById(1); + + expect(result).toEqual(mockUser); + expect(userRepository.findById).toHaveBeenCalledWith(1); + }); + + it("should create a new user", async () => { + userRepository.findByEmail.mockResolvedValue(null); + const newUser: User = { id: 1, email: "new@example.com", name: "New User" }; + userRepository.save.mockResolvedValue(newUser); + + const result = await userService.createUser("new@example.com", "New User"); + + expect(result).toEqual(newUser); + expect(userRepository.findByEmail).toHaveBeenCalledWith("new@example.com"); + expect(userRepository.save).toHaveBeenCalled(); + }); + + it("should throw error if user already exists", async () => { + const existingUser: User = { + id: 1, + email: "existing@example.com", + name: "Existing", + }; + userRepository.findByEmail.mockResolvedValue(existingUser); + + await expect( + userService.createUser("existing@example.com", "New Name") + ).rejects.toThrow("User already exists"); + + expect(userRepository.save).not.toHaveBeenCalled(); + }); +}); +``` + +## Using Entity Manager Directly + +If you need to use TypeORM's EntityManager for transactions or complex queries: + +```typescript title="transaction.service.ts" +import { Injectable } from "@nestjs/common"; +import { EntityManager } from "typeorm"; + +@Injectable() +export class TransactionService { + constructor(private readonly entityManager: EntityManager) {} + + async executeInTransaction( + callback: (manager: EntityManager) => Promise + ): Promise { + return this.entityManager.transaction(callback); + } +} +``` + +```typescript title="transaction.service.spec.ts" +import { TestBed, type Mocked } from "@suites/unit"; +import { TransactionService } from "./transaction.service"; +import { EntityManager } from "typeorm"; + +describe("TransactionService", () => { + let transactionService: TransactionService; + let entityManager: Mocked; + + beforeAll(async () => { + const { unit, unitRef } = await TestBed.solitary( + TransactionService + ).compile(); + transactionService = unit; + entityManager = unitRef.get(EntityManager); + }); + + it("should execute callback in transaction", async () => { + const callback = jest.fn().mockResolvedValue("result"); + entityManager.transaction.mockImplementation(async (fn) => + fn(entityManager) + ); + + const result = await transactionService.executeInTransaction(callback); + + expect(result).toBe("result"); + expect(entityManager.transaction).toHaveBeenCalled(); + expect(callback).toHaveBeenCalledWith(entityManager); + }); +}); +``` + +## Summary + +- **Wrap TypeORM repositories** in injectable classes to make them mockable +- **Use Suites** to automatically mock repository dependencies in your service tests +- **EntityManager** can be injected directly for transactions and complex queries +- **Keep repositories focused** on data access, separating concerns from business logic + +## Next Steps + +- **[Solitary Unit Tests](/docs/guides/solitary)**: Deep dive into testing in isolation +- **[Test Doubles](/docs/guides/test-doubles)**: Understand mocks and stubs in depth