Jest testing DTO decorators. Fails in Jest SPEC file, but works via Postman

Viewed 914

Test case works in Postman, but not from Visual Studio Code or Jest command line.

Request Body in Postman that works and returns all 4 errors:

{ "Item": {} }

This is missing the FileName and Item.Data fields.

API.dto.ts

import { Type } from 'class-transformer';
import { IsNumberString, IsString, MinLength, ValidateNested } from 'class-validator';

// Expected Payload
// {
//  FileName: 'abc',
//  Item: {
//      Data: '123'
//  }
// }

export class ItemDataDTO {
    @IsNumberString() @MinLength(2) public readonly Data: string;
}
/**
 * This class is the Data Object for the API route
 */
export class ApiDTO {
    @IsString() @MinLength(1) public readonly FileName: string;

    @ValidateNested()
    @Type(() => ItemDataDTO)
    Item: ItemDataDTO;
}

api.controller.ts

import { Body, Controller, Get, Options, Put, Request, Response } from '@nestjs/common';
import { ApiDTO } from './api.dto';

@Controller('api')
export class ApiController {
    constructor() { }

    @Put('donotuse')
    public DoNotUse(@Body() APIBody: ApiDTO) {
        return 'OK';
    }
}

API.dto.spec.ts

import { ArgumentMetadata, ValidationPipe } from '@nestjs/common';
import { ApiDTO } from './api.dto';

describe('ApiDto', () => {
    it('should be defined', () => {
        expect(new ApiDTO()).toBeDefined();
    });

    it('should validate the ApiDTO definition', async () => {
        const target: ValidationPipe = new ValidationPipe({
            transform: true,
            whitelist: true,
        });
        const metadata: ArgumentMetadata = {
            type: 'body',
            metatype: ApiDTO,
            data: '{ "Item": {} }',
        };
        const Expected: string[] = [
            'FileName must be longer than or equal to 1 characters',
            'FileName must be a string',
            'Item.Data must be longer than or equal to 2 characters',
            'Item.Data must be a number string',
        ];
        await target.transform(<ApiDTO>{}, metadata).catch((err) => {
            expect(err.getResponse().message).toEqual(Expected);
        });
    });
});

The expect fails.

await target.transform(<ApiDTO>{}, metadata).catch((err) => { 
    expect(err.getResponse().message).toEqual(Expected);
});

The 2 FileName errors are returned, but no the Item.Data fields. Setting data: '', to be data: '{ "Item": {} }' also fails the same way.

Actual expectation failure:

expect(received).toEqual(expected) // deep equality
    - Expected  - 2
    + Received  + 0

  Array [
    "FileName must be longer than or equal to 1 characters",
    "FileName must be a string",
-   "Item.Data must be longer than or equal to 2 characters",
-   "Item.Data must be a number string",
  ]

This is indicating that the FileName validation is there, those 2 lines are returned, but the Item.Data errors, are not coming back, and are 'extra' in my test case results.

However, calling this via Postman, PUT /api/donotuse with the request body:

{ "Item": {} }

returns all 4 of the errors. The HTTP Status code is also a 400 Bad Request, as NestJS would normally return on its own. I am not sure what is wrong in my test case to get the errors to all be returned.

EDIT I have also then tried to do this via E2E testing as the answer suggested, but I still receive the same missing errors.

describe('ApiDto - E2E', () => {
    let app: INestApplication;

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

    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();

        app = moduleFixture.createNestApplication();
        app.useGlobalPipes(new ValidationPipe({ transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }));
        await app.init();
    });

    it('should validate the ApiDTO definition', async () => {
        const APIRequestDTO: unknown = { FileName: null, Item: {} };
        const ResponseData$ = await request(app.getHttpServer())
            .put('/api/donotuse')
            .set('Accept', 'application/json')
            .send(APIRequestDTO as ApiDTO);

        const Expected: string[] = [
            'FileName must be longer than or equal to 1 characters',
            'FileName must be a string',
            'Item.Data must be longer than or equal to 2 characters',
            'Item.Data must be a number string',
        ];

        expect(ResponseData$.status).toBe( HttpStatus.UNPROCESSABLE_ENTITY);
        expect(ResponseData$.body.message).toBe(Expected);
    });
});

This still does not provide all the errors, that are properly returned from the Postman call. I am not sure what is happening during testing that the sub type is not processed. Calling this via Postman, same body, same headers, etc., does return the proper errors:

"message": [ "FileName must be longer than or equal to 1 characters", "FileName must be a string", "Item.Data must be longer than or equal to 2 characters", "Item.Data must be a number string" ],

I know it is going into the ValidationPipe as well, as my custom error code, 422 Unprocessable Entity is returned, indicating this is the validation that is failing. This same error is returned in both my unit test and the E2E test, but not the second set of errors about Item.Data.

1 Answers

I assume that in your app you're registering the ValidationPipe globally, eg: app.useGlobalPipes(new ValidationPipe());

Due to the location of where Global Pipes are registered, they will work when you execute an actual request against your backend but will not be picked up in tests. This is why you're seeing it working through Postman, but not through Jest.

If you want the Validation pipe to be used in your tests you will need to manually set it up like so:

// Probably in your beforeEach where you're setting up the test module
const app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());

Duplicate of How to apply Global Pipes during e2e tests

Related