---
title: "Unit and e2e testing in NestJS with Jest and Supertest"
description: "NestJS testing runs through its DI system, so mocks and app boot differ from plain Jest. The unit and e2e setup with Jest and Supertest that actually works."
canonical: https://omiid.me/notebook/18/unit-and-e2e-testing-in-nestjs-with-jest-and-supertest
author: Omid Sayfun
published: 2026-05-06
updated: 2026-08-09
---

# Unit and e2e testing in NestJS with Jest and Supertest

Testing in NestJS isn't hard once you understand that everything goes through the DI container. The `@nestjs/testing` package lets you build a minimal version of your app (just the providers you care about, with real or mocked dependencies) and run tests against it.

The first decision is unit test or e2e.

- Unit tests: boot a lightweight module with mocked dependencies. Fast, isolated, good for service logic and edge cases.
- E2e tests: boot the full app and hit it with HTTP via Supertest. Slower, but tests the whole request lifecycle including guards, pipes, and interceptors.

Most projects need both. Unit tests for logic, e2e for the critical paths.

## Unit test a service by mocking its dependencies

Say you have an `OrdersService` that depends on a `PaymentsService`:

```typescript
@Injectable()
export class OrdersService {
	constructor(private readonly paymentsService: PaymentsService) {}

	async placeOrder(itemId: string, amount: number) {
		const charged = await this.paymentsService.charge(amount);
		if (!charged) throw new Error("Payment failed");
		return { itemId, status: "confirmed" };
	}
}
```

To test this without spinning up the full app:

```typescript
import { Test, TestingModule } from "@nestjs/testing";
import { OrdersService } from "./orders.service";
import { PaymentsService } from "../payments/payments.service";

describe("OrdersService", () => {
	let service: OrdersService;
	let paymentsService: jest.Mocked<PaymentsService>;

	beforeEach(async () => {
		const module: TestingModule = await Test.createTestingModule({
			providers: [
				OrdersService,
				{
					provide: PaymentsService,
					useValue: { charge: jest.fn() },
				},
			],
		}).compile();

		service = module.get<OrdersService>(OrdersService);
		paymentsService = module.get(PaymentsService);
	});

	it("confirms order when payment succeeds", async () => {
		paymentsService.charge.mockResolvedValue(true);
		const result = await service.placeOrder("item-1", 100);
		expect(result.status).toBe("confirmed");
	});

	it("throws when payment fails", async () => {
		paymentsService.charge.mockResolvedValue(false);
		await expect(service.placeOrder("item-1", 100)).rejects.toThrow(
			"Payment failed",
		);
	});
});
```

`Test.createTestingModule()` builds a minimal NestJS testing module. `{ provide: PaymentsService, useValue: ... }` replaces the real service with your mock for everything in that module. `module.get()` retrieves the instance from the container.

If you run NestJS with Vitest instead of Jest, the same `Test.createTestingModule()` API applies. Swap `jest.fn()` for `vi.fn()`.

## Unit test a controller with a mocked service

Controllers depend on services, so the setup is the same. Mock the services, register the controller:

```typescript
describe("OrdersController", () => {
	let controller: OrdersController;
	let ordersService: jest.Mocked<OrdersService>;

	beforeEach(async () => {
		const module: TestingModule = await Test.createTestingModule({
			controllers: [OrdersController],
			providers: [
				{
					provide: OrdersService,
					useValue: { placeOrder: jest.fn() },
				},
			],
		}).compile();

		controller = module.get<OrdersController>(OrdersController);
		ordersService = module.get(OrdersService);
	});

	it("returns order from service", async () => {
		ordersService.placeOrder.mockResolvedValue({
			itemId: "item-1",
			status: "confirmed",
		});
		const result = await controller.placeOrder({
			itemId: "item-1",
			amount: 100,
		});
		expect(result.status).toBe("confirmed");
	});
});
```

Unit testing a controller is useful for checking it calls the right service method with the right arguments. For testing the HTTP layer (status codes, validation pipes, guards), e2e is a better fit.

## Pick useValue, useFactory, or spyOn for your mocks

**`useValue`** is the simplest. Pass a plain object with `jest.fn()` methods. Use this for most unit tests.

**`useFactory`** is useful when the mock needs to read from another provider:

```typescript
{
  provide: PaymentsService,
  useFactory: (config: ConfigService) => ({
    charge: jest.fn().mockResolvedValue(config.get('MOCK_CHARGE_RESULT')),
  }),
  inject: [ConfigService],
}
```

**`jest.spyOn`** is for when you want the real implementation but need to intercept specific calls:

```typescript
jest.spyOn(paymentsService, "charge").mockResolvedValue(true);
```

Decision rule: use `useValue` by default. Use `useFactory` when the mock depends on an injected value. Use `spyOn` when you want to partially mock a real instance.

Mocking `ConfigService` comes up constantly. A quick pattern that covers most cases:

```typescript
{
  provide: ConfigService,
  useValue: {
    get: jest.fn((key: string) => ({ DATABASE_URL: 'postgres://localhost/test' })[key]),
  },
}
```

For context on how `ConfigService` is typically wired up with Zod validation, [this post on NestJS env vars with Zod](https://omiid.me/notebook/38/validating-nestjs-env-vars-with-zod) covers the setup you'd be mocking here.

## Run end-to-end tests against the whole app with Supertest

E2e tests boot the actual NestJS app and send HTTP requests through it with Supertest. Instead of scattering `moduleRef.get()` calls and loose `let` declarations across a `describe` block, put the whole setup in a factory function that returns everything the test file needs:

```typescript
// test/helpers/create-orders-app.ts
import { Test } from "@nestjs/testing";
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { AppModule } from "../../src/app.module";
import { OrdersService } from "../../src/orders/orders.service";

export async function createOrdersTestApp() {
	const moduleRef = await Test.createTestingModule({
		imports: [AppModule],
	}).compile();

	const app = moduleRef.createNestApplication();
	app.useGlobalPipes(new ValidationPipe()); // match your main.ts setup
	await app.init();

	return {
		app,
		moduleRef,
		ordersService: moduleRef.get<OrdersService>(OrdersService),
	};
}
```

The test file stays clean. Destructure what you need, nothing more:

```typescript
import * as request from "supertest";
import { INestApplication } from "@nestjs/common";
import { OrdersService } from "../src/orders/orders.service";
import { createOrdersTestApp } from "./helpers/create-orders-app";

describe("Orders (e2e)", () => {
	let app: INestApplication;
	let ordersService: OrdersService;

	beforeAll(async () => {
		({ app, ordersService } = await createOrdersTestApp());
	});

	afterAll(async () => {
		await app.close();
	});

	it("POST /orders returns 201", () => {
		return request(app.getHttpServer())
			.post("/orders")
			.send({ itemId: "item-1", amount: 100 })
			.expect(201)
			.expect((res) => {
				expect(res.body.status).toBe("confirmed");
			});
	});
});
```

`app.useGlobalPipes()` and other global setup from `main.ts` won't be applied automatically in tests, so add it manually in the factory.

**Choose between a real test database and a mocked repository**

Real test database: spin up a separate Postgres instance (Docker works well), run migrations before the suite, truncate tables between tests. You know your queries work against real Postgres. The tradeoff is setup complexity and slower CI.

Mocking the repository: override the TypeORM/Drizzle/Prisma provider before compiling. Faster and simpler, but you're not testing the actual queries.

For TypeORM testing, `.overrideProvider()` slots into the factory cleanly:

```typescript
export async function createOrdersTestApp() {
	const moduleRef = await Test.createTestingModule({
		imports: [AppModule],
	})
		.overrideProvider(getRepositoryToken(Order))
		.useValue({
			find: jest.fn().mockResolvedValue([]),
			save: jest.fn().mockResolvedValue({ id: 1, status: "confirmed" }),
		})
		.compile();

	const app = moduleRef.createNestApplication();
	app.useGlobalPipes(new ValidationPipe());
	await app.init();

	return { app, moduleRef };
}
```

For Drizzle, override whichever provider wraps your `db` instance.

A pragmatic split: mock the DB in most e2e tests, keep one or two integration tests that hit a real database for the critical data paths.

## Override guards and interceptors inside the factory

Override the guard inside the factory before compiling:

```typescript
export async function createOrdersTestApp() {
	const moduleRef = await Test.createTestingModule({
		imports: [AppModule],
	})
		.overrideGuard(AuthGuard)
		.useValue({ canActivate: () => true })
		.compile();

	const app = moduleRef.createNestApplication();
	await app.init();

	return { app, moduleRef };
}
```

If you want the guards active so you can test the real auth flow, keep them in and either generate a real JWT in `beforeAll` or mock the `JwtService` via `.overrideProvider()` in the factory.

Testing an interceptor in isolation:

```typescript
import { of, lastValueFrom } from "rxjs";

const interceptor = new LoggingInterceptor();
const mockContext = createMock<ExecutionContext>(); // @golevelup/ts-jest
const mockNext = { handle: () => of("response") };

const result = await lastValueFrom(
	interceptor.intercept(mockContext, mockNext),
);
expect(result).toBe("response");
```

`createMock` from `@golevelup/ts-jest` saves a lot of boilerplate when you need a typed mock of `ExecutionContext` or `ArgumentsHost`, which have many methods to stub otherwise.

---

If you're testing a NestJS microservice setup specifically (with `ClientProxy` mocks on the client side and `@MessagePattern` handlers on the server), [the NestJS microservices post](https://omiid.me/notebook/21/nestjs-microservices-with-a-real-two-service-example) shows the full two-service architecture you'd be writing tests against.
