24

TLNR: I was trying to test DTO validation in the controller spec instead of in e2e specs, which are precisely crafted for that. McDoniel's answer pointed me to the right direction.


I develop a NestJS entrypoint, looking like that:

@Post()
async doStuff(@Body() dto: MyDto): Promise<string> {
  // some code...
}

I use class-validator so that when my API receives a request, the payload is parsed and turned into a MyDto object, and validations present as annotations in MyDto class are performed. Note that MyDto has an array of nested object of class MySubDto. With the @ValidateNested and @Type annotations, the nested objects are also validated correctly.

This works great.

Now I want to write tests for the performed validations. In my .spec file, I write:

import { validate  } from 'class-validator';
// ...
it('should FAIL on invalid DTO', async () => {
  const dto = {
    //...
  };
  const errors = await validate( dto );
  expect(errors.length).not.toBe(0);
}

This fails because the validated dto object is not a MyDto. I can rewrite the test as such:

it('should FAIL on invalid DTO', async () => {
  const dto = new MyDto()
  dto.attribute1 = 1;
  dto.subDto = { 'name':'Vincent' };
  const errors = await validate( dto );
  expect(errors.length).not.toBe(0);
}

Validations are now properly made on the MyDto object, but not on my nested subDto object, which means I will have to instantiate aaaall objects of my Dto with according classes, which would be much inefficient. Also, instantiating classes means that TypeScript will raise errors if I voluntarily omits some required properties or indicate incorrect values.

So the question is:

How can I use NestJs built-in request body parser in my tests, so that I can write any JSON I want for dto, parse it as a MyDto object and validate it with class-validator validate function?

Any alternate better-practice ways to tests validations are welcome too!

2 Answers 2

44

Although, we should test how our validation DTOs work with ValidationPipe, that's a form of integration or e2e tests. Unit tests are unit tests, right?! Every unit should be testable independently.

The DTOs in Nest.js are perfectly unit-tastable. It becomes necessary to unit-test the DTOs, when they contain complex regular expressions or sanitation logic.


Creating an object of the DTO for test

The request body parser in Nest.js that you are looking for is the class-transformer package. It has a function plainToInstance() to turn your literal or JSON object into an object of the specified type. In your example the specified type is the type of your DTO:

const myDtoObject = plainToInstance(MyDto, myBodyObject)

Here, myBodyObject is your plain object that you created for test, like:

const myBodyObject = { attribute1: 1, subDto: { name: 'Vincent' } }

The plainToInstance() function also applies all the transformations that you have in your DTO. If you just want to test the transformations, you can assert after this statement. You don't have to call the validate() function to test the transformations.


Validating the object of the DTO in test

To the emulate validation of Nest.js, simply pass the myDtoObject to the validate() function of the class-validator package:

const errors = await validate(myDtoObject)

Also, if your DTO or SubDTO object is too big or too complex to create, you have the option to skip the remaining properties or subObjects like your subDto:

const errors = await validate(myDtoObject, { skipMissingProperties: true })

Now your test object could be without the subDto, like:

const myBodyObject = { attribute1: 1 }

Asserting the errors

Apart from asserting that the errors array is not empty, I also like to specify a custom error message for each validation in the DTO:

@IsPositive({ message: `Attribute1 must be a positive number.` })
readonly attribute1: number

One advantage of a custom error message is that we can write it in a user-friendly way instead of the generic messages created by the library. Another big advantage is that I can assert this error message in my tests. This way I can be sure that the errors array is not empty because it contains the error for this particular validation and not something else:

expect(stringified(errors)).toContain(`Attribute1 must be a positive number.`)

Here, stringified() is a simple utility function to convert the errors object to a JSON string, so we can search our error message in it:

export function stringified(errors: ValidationError[]): string {
  return JSON.stringify(errors)
}

Your final test code

Instead of the controller.spec.ts file, create a new file specific to your DTO, like my-dto.spec.ts for unit tests of your DTO. A DTO can have plenty of unit tests and they should not be mixed with the controller's tests:

it('should fail on invalid DTO', async () => {
  const myBodyObject = { attribute1: -1, subDto: { name: 'Vincent' } }
  const myDtoObject = plainToInstance(MyDto, myBodyObject)
  const errors = await validate(myDtoObject)
  expect(errors.length).not.toBe(0)
  expect(stringified(errors)).toContain(`Attribute1 must be a positive number.`)
}

Notice how you don't have to assign the values to the properties one by one for creating the myDtoObject. In most cases, the properties of your DTOs should be marked readonly. So, you can't assign the values one by one. The plainToInstance() to the rescue!


That's it! You were almost there, unit testing your DTO. Good efforts! Hope that helps now.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you for a complete reply! I learned a couple of useful things here. Now, this leaves me with 2 additional questions. A/ with this method, how do you additionnally assert that validations will actually be applied by the controller?
@Bob, A/ How the Controller interacts with the DTO should be tested in e2e or integration tests, because it's an integration between the DTO and Controller. Simply pass the invalid input in the Supertest and assert that it throws your specified error.
@Bob, B/ sounds like your business logic. To check If some input meets your business requirements, should be analysed and validated inside your Service and throw errors from there. Controllers should be kept dumb. They should not include any logic nor the validations. Their main job is routing and parsing input. Simply forward the request from the controllers to the Service where you include all the business related validations and other logic. DTO validations should only include universal validations like username min length, max length or sanitations like trimming spaces from input fields.
This answer deserves more upvotes
Great explanation, thank you for the verbose explanation.
|
19

To test input validation with the validation pipes, I think it is agreed that the best place to do this is in e2e tests rather than in unit tests, just make sure that you remember to register your pipes (if you normally use app.useGlobalPipes() instead of using dependency injection)

4 Comments

How would one go about that registering pipes approach?
As mentioned, if you normally use app.useGlobalPipes() in your main.ts, you'll need to do the same in an e2e test, as the RootTestModule does not run through the main.ts (nor should it, as it is a completely different context)
Thanks!! This is what I was looking for. And it makes much more sense to test it in the e2e tests.
Hey, I disagree with you, because you may want to control transformed values and improve test coverage

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.