GraphQL with NestJS
Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.
Testing GraphQL APIs with NestJS
This document outlines how to effectively test GraphQL APIs built with the NestJS framework, focusing on unit and integration testing of resolvers and services. We will explore the use of tools like Jest and Supertest for testing GraphQL endpoints.
Understanding the Testing Landscape in NestJS GraphQL
Testing GraphQL APIs in NestJS requires a multi-faceted approach to ensure that your application behaves as expected. This includes:
- Unit Testing: Verifying individual components (resolvers, services) in isolation.
- Integration Testing: Testing the interaction between multiple components, such as resolvers and services.
- End-to-End (E2E) Testing: Testing the entire application from the client's perspective, often involving a running NestJS application. (This is outside the scope of this focused guide, but keep it in mind.)
Unit Testing GraphQL Resolvers and Services
Unit tests focus on verifying the logic within individual resolvers and services without involving external dependencies like databases or other services. We'll use Jest for this.
Example: Testing a User Service
Let's assume we have a UserService
with a method to fetch a user by ID:
// src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
@Injectable()
export class UserService {
async findOne(id: number): Promise<User | undefined> {
// Simulate fetching from a database
const users = [{ id: 1, name: 'John Doe', email: 'john.doe@example.com' }];
return users.find(user => user.id === id);
}
}
Here's a unit test using Jest:
// src/user/user.service.spec.ts
import { UserService } from './user.service';
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
it('should be defined', () => {
expect(userService).toBeDefined();
});
it('should return a user when findOne is called with a valid id', async () => {
const user = await userService.findOne(1);
expect(user).toEqual({ id: 1, name: 'John Doe', email: 'john.doe@example.com' });
});
it('should return undefined when findOne is called with an invalid id', async () => {
const user = await userService.findOne(999);
expect(user).toBeUndefined();
});
});
Explanation:
- We import the
UserService
. beforeEach
creates a new instance of the service before each test.- The tests call the
findOne
method with different inputs and assert the expected output.
Mocking Dependencies
If your service relies on a database connection or another service, you'll want to mock those dependencies to isolate the unit being tested. Jest provides excellent mocking capabilities.
Integration Testing GraphQL Resolvers and Services
Integration tests verify that resolvers and services work together correctly. This typically involves setting up a testing module that mimics your application module.
Example: Testing a User Resolver
Let's assume we have a UserResolver
that uses the UserService
to fetch user data:
// src/user/user.resolver.ts
import { Resolver, Query, Args, Int } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => User, { name: 'user' })
async getUser(@Args('id', { type: () => Int }) id: number): Promise<User | undefined> {
return this.userService.findOne(id);
}
}
Here's an integration test using Jest and NestJS's TestingModule
:
// src/user/user.resolver.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
import { User } from './user.entity';
describe('UserResolver', () => {
let resolver: UserResolver;
let userService: UserService;
const mockUserService = {
findOne: jest.fn().mockImplementation((id: number) => {
if (id === 1) {
return Promise.resolve({ id: 1, name: 'John Doe', email: 'john.doe@example.com' } as User);
}
return Promise.resolve(undefined);
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserResolver,
{
provide: UserService,
useValue: mockUserService,
},
],
}).compile();
resolver = module.get(UserResolver);
userService = module.get(UserService);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
it('should return a user when getUser is called with a valid id', async () => {
const user = await resolver.getUser(1);
expect(user).toEqual({ id: 1, name: 'John Doe', email: 'john.doe@example.com' });
expect(userService.findOne).toHaveBeenCalledWith(1);
});
it('should return undefined when getUser is called with an invalid id', async () => {
const user = await resolver.getUser(999);
expect(user).toBeUndefined();
expect(userService.findOne).toHaveBeenCalledWith(999);
});
});
Explanation:
- We use
Test.createTestingModule
to create a mock NestJS module. - We provide a mock implementation of the
UserService
usinguseValue
. This is crucial for isolating the resolver and controlling its behavior. - We get instances of the
UserResolver
and the mockedUserService
from the module. - The tests call the
getUser
resolver method and assert the expected output. We also verify that thefindOne
method of the mockedUserService
was called with the correct arguments.
Testing GraphQL Endpoints with Supertest
Supertest allows you to send HTTP requests to your GraphQL endpoint and assert the responses. This is crucial for testing the entire GraphQL API from an external perspective.
Example: Using Supertest to query the GraphQL endpoint
First, you need to start your NestJS application for testing. For instance, create a separate testing module that bootstraps the application on a dedicated test port. Let's assume you have this setup (details on setting up an e2e/integration test environment with NestJS is beyond this focused scope, but it's essential).
Here's an example of a Supertest integration test:
// src/app.e2e-spec.ts (Assuming this is your end-to-end testing file)
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { AppModule } from './app.module'; // Or your main app module
import { INestApplication } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
describe('GraphQL API (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule], // Import your main application module
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('should return a user when querying with a valid id', async () => {
const query = `
query {
user(id: 1) {
id
name
email
}
}
`;
return request(app.getHttpServer())
.post('/graphql') // Or your GraphQL endpoint
.send({ query })
.expect(200)
.expect((res) => {
expect(res.body.data.user).toEqual({
id: '1', // Supertest returns strings for IDs, be mindful of this
name: 'John Doe',
email: 'john.doe@example.com',
});
});
});
it('should return null when querying with an invalid id', async () => {
const query = `
query {
user(id: 999) {
id
name
email
}
}
`;
return request(app.getHttpServer())
.post('/graphql')
.send({ query })
.expect(200)
.expect((res) => {
expect(res.body.data.user).toBeNull();
});
});
});
Explanation:
- We import
supertest
and create a test module that imports your application module. - We bootstrap the NestJS application using
app.init()
. - The tests send a POST request to the
/graphql
endpoint (or whatever endpoint you've configured). - The request body contains the GraphQL query.
- We assert the HTTP status code (200) and the structure and content of the response body. Note the ID is returned as a *string* by Supertest.
- The
toBeNull()
matcher tests for null results from an invalid ID.
Best Practices for Testing GraphQL APIs in NestJS
- Write tests early and often: Adopt a test-driven development (TDD) approach.
- Focus on edge cases and error handling: Ensure your application gracefully handles unexpected inputs and errors.
- Keep tests independent: Avoid dependencies between tests to prevent cascading failures.
- Use descriptive test names: Make it clear what each test is verifying.
- Use environment variables for configuration: Configure database connections and other environment-specific settings using environment variables in your test environment.
- Consider using code coverage tools: Track the percentage of your code that is covered by tests. Aim for high coverage, but don't sacrifice quality for quantity.