diff --git a/docs/recipes/index.md b/docs/recipes/index.md new file mode 100644 index 0000000..39731a4 --- /dev/null +++ b/docs/recipes/index.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 3 +title: Recipes +description: Practical testing recipes for NestJS with Suites, covering ORMs, databases, and real-world patterns. +keywords: [suites, nestjs testing, unit testing recipes, mock database, typescript testing] +--- + +# Recipes + +> **What this covers:** Practical examples for testing NestJS services with Suites \ +> **Best for:** Developers looking for patterns beyond the core guides + +This section contains real-world examples showing how to use Suites with popular libraries and frameworks. Each recipe walks through the pattern, implementation, and test code. + +:::tip Examples Repository +For complete, runnable code examples, browse the [Suites Examples repository](https://github.com/suites-dev/examples). +::: + +## Available Recipes + +- **[Mocking ORMs](/docs/recipes/mocking-orm)** - Mock TypeORM, Prisma, Drizzle, and MikroORM in NestJS unit tests diff --git a/docs/recipes/mocking-orm/drizzle.md b/docs/recipes/mocking-orm/drizzle.md new file mode 100644 index 0000000..59ecf94 --- /dev/null +++ b/docs/recipes/mocking-orm/drizzle.md @@ -0,0 +1,159 @@ +--- +sidebar_position: 11 +title: "Mocking Drizzle ORM in NestJS Unit Tests" +description: Mock Drizzle database instances in NestJS unit tests using Suites. Wrap the Drizzle client in an injectable service for clean, isolated testing with TypeScript. +keywords: [mock drizzle, drizzle orm unit test, nestjs drizzle testing, mock database, typescript testing, suites] +--- + +# Mocking Drizzle in NestJS Unit Tests + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +Drizzle uses a database instance that you typically import directly. Wrap it in an injectable class so Suites can auto-mock it. + +## 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" + ); + }); +}); +``` + +## 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 + +## 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..0105b07 --- /dev/null +++ b/docs/recipes/mocking-orm/index.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 8 +title: "Mocking ORMs in NestJS Unit Tests: TypeORM, Prisma, Drizzle, MikroORM" +description: Mock TypeORM repositories, Prisma client, Drizzle, and MikroORM EntityManager in NestJS unit tests using Suites. Step-by-step patterns with TypeScript examples. +keywords: [mock typeorm, mock prisma, mock drizzle, mock mikroorm, nestjs unit test, mock database, typescript testing, suites] +--- + +# Mocking ORMs in NestJS Unit Tests + +> **What this covers:** Mocking ORM libraries (TypeORM, Prisma, Drizzle, MikroORM) in NestJS services \ +> **Time to read:** ~12 minutes \ +> **Prerequisites:** [Unit Testing Fundamentals](/docs/guides/fundamentals), [Solitary Unit Tests](/docs/guides/solitary) \ +> **Best for:** Testing NestJS services that interact with databases without hitting real connections + +When testing NestJS 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..7f7bfb4 --- /dev/null +++ b/docs/recipes/mocking-orm/mikroorm.md @@ -0,0 +1,209 @@ +--- +sidebar_position: 12 +title: "Mocking MikroORM in NestJS Unit Tests" +description: Mock MikroORM EntityManager and repositories in NestJS unit tests using Suites. Wrap data access in injectables for clean, isolated testing with TypeScript. +keywords: [mock mikroorm, mikroorm unit test, nestjs mikroorm testing, mock entity manager, typescript testing, suites] +--- + +# Mocking MikroORM in NestJS Unit Tests + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +MikroORM uses entity managers and repositories to interact with the database. Wrap these in injectable classes so Suites can auto-mock them. + +See also 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..1a42172 --- /dev/null +++ b/docs/recipes/mocking-orm/prisma.md @@ -0,0 +1,164 @@ +--- +sidebar_position: 10 +title: "Mocking Prisma Client in NestJS Unit Tests" +description: Mock PrismaClient and PrismaService in NestJS unit tests using Suites. Wrap the generated client in an injectable for clean, isolated testing with TypeScript. +keywords: [mock prisma, prisma unit test, nestjs prisma testing, mock prisma client, prisma service mock, typescript testing, suites] +--- + +# Mocking Prisma in NestJS Unit Tests + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +Prisma uses a generated client that you typically import directly. Wrap it in an injectable class so Suites can auto-mock it. + +See also 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", + }); + }); +}); +``` + +## 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 + +## 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..927b50d --- /dev/null +++ b/docs/recipes/mocking-orm/typeorm.md @@ -0,0 +1,203 @@ +--- +sidebar_position: 9 +title: "Mocking TypeORM Repositories in NestJS Unit Tests" +description: Mock TypeORM Repository and EntityManager in NestJS unit tests using Suites. Wrap repositories in injectables for clean, isolated testing with TypeScript. +keywords: [mock typeorm, typeorm unit test, nestjs typeorm testing, mock repository, mock entity manager, typescript testing, suites] +--- + +# Mocking TypeORM in NestJS Unit Tests + +:::info Overview +For an overview of the pattern and approach to mocking ORMs, see the [Mocking ORMs overview](/docs/recipes/mocking-orm). +::: + +TypeORM uses repositories and entity managers to interact with the database. Wrap these in injectable classes so Suites can auto-mock them. + +See also 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