Search code examples
jestjsnestjs

How to test Nestjs routes with @Query decorators and Validation Pipes?


Imagine I have a Controller defined like so:

class NewsEndpointQueryParameters {
  @IsNotEmpty()
  q: string;

  @IsNotEmpty()
  pageNumber: number;
}

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get(['', 'ping'])
  ping(): PingEndpointResponse {
    return this.appService.ping();
  }

  @Get(['news'])
  getNews(
    @Query() queryParameters: NewsEndpointQueryParameters
  ): Observable<NewsEndpointResponse> {
    return this.appService.getNews(
      queryParameters.q,
      queryParameters.pageNumber
    );
  }
}

I want to be able to test what happens in a request, if, for example, a query parameter is not provided.

Right now this is my testing setup:

describe('AppController', () => {
  let app: TestingModule;
  let nestApp: INestApplication;
  let appService: AppService;

  beforeAll(async () => {
    app = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
      imports: [HttpModule],
    }).compile();
    appService = app.get<AppService>(AppService);
    nestApp = app.createNestApplication();
    await nestApp.init();
    return;
  });

  describe('/', () => {
    test('Return "Pong!"', async () => {
      const appServiceSpy = jest.spyOn(appService, 'ping');
      appServiceSpy.mockReturnValue({ message: 'Pong!' });
      const response = await supertest(nestApp.getHttpServer()).get('/');
      expect(response.body).toStrictEqual({
        message: 'Pong!',
      });
      return;
    });
  });

  describe('/ping', () => {
    test('Return "Pong!"', async () => {
      const appServiceSpy = jest.spyOn(appService, 'ping');
      appServiceSpy.mockReturnValue({ message: 'Pong!' });
      const response = await supertest(nestApp.getHttpServer()).get('/ping');
      expect(response.body).toStrictEqual({
        message: 'Pong!',
      });
      return;
    });
  });

  describe('/news', () => {
    describe('Correct query', () => {
      beforeEach(() => {
        const appServiceSpy = jest.spyOn(appService, 'getNews');
        appServiceSpy.mockReturnValue(
          new Observable<NewsEndpointResponse>((subscriber) => {
            subscriber.next({
              data: [{ url: 'test' }],
              message: 'test',
              status: 200,
            });
            subscriber.complete();
          })
        );
        return;
      });

      test('Returns with a custom body response.', async () => {
        const response = await supertest(nestApp.getHttpServer()).get(
          '/news?q=test&pageNumber=1'
        );
        expect(response.body).toStrictEqual({
          data: [{ url: 'test' }],
          message: 'test',
          status: 200,
        });
        return;
      });

      return;
    });

    describe('Incorrect query', () => {
      test("Returns an error if 'q' query parameter is missing.", async () => {
        return;
      });

      test("Returns an error if 'pageNumber' query parameter is missing.", async () => {
        return;
      });

      return;
    });

    return;
  });

  return;
});

If I do nx serve and then curl 'localhost:3333/api/ping', I get:

{"message":"Pong!"}

And if I do curl 'localhost:3333/api/news?q=test&pageNumber=1' I get:

{"data":['lots of interesting news'],"message":"News fetched successfully!","status":200}

Finally, if I do curl 'localhost:3333/api/news?q=test' I get:

{"statusCode":400,"message":["pageNumber should not be empty"],"error":"Bad Request"}

How can I replicate the last case? If I use supertest, there is no error returned like the above. I haven't found a way to mock the Controller's function too.


Solution

  • A very special thank to @jmcdo29 for explaining me how to do this.

    Code:

    beforeAll(async () => {
      app = await Test.createTestingModule({
        controllers: [AppController],
        providers: [
          AppService,
          { provide: APP_PIPE, useValue: new ValidationPipe() },
        ],
        imports: [HttpModule, AppModule],
      }).compile();
      appService = app.get<AppService>(AppService);
      nestApp = app.createNestApplication();
      await nestApp.init();
      return;
    });
    

    Explanation:

    • We need to model the behavior of bootstrap() in main.ts. In my case, in looks like this:

      async function bootstrap() {
        const app = await NestFactory.create(AppModule, {
          cors: environment.nestCors,
        });
        app.useGlobalPipes(new ValidationPipe());
        const globalPrefix = 'api';
        app.setGlobalPrefix(globalPrefix);
        const port = process.env.PORT || 3333;
        await app.listen(port, () => {
          Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix);
        });
      }
      

    Instead of importing the AppModule, we could also configure the app created for testing like so: nestApp.useGlobalPipes(new ValidationPipe()) (this needs to be done before await nestApp.init())