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 using useValue. This is crucial for isolating the resolver and controlling its behavior.
  • We get instances of the UserResolver and the mocked UserService from the module.
  • The tests call the getUser resolver method and assert the expected output. We also verify that the findOne method of the mocked UserService 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.