如何使用 NestJS 和 class-validator 手动测试输入验证
How to manually test input validation with NestJS and class-validator
TLNR: 我试图在控制器规范中测试 DTO 验证,而不是在专门为此设计的 e2e 规范中。 McDoniel 的回答为我指明了正确的方向。
我开发了一个 NestJS 入口点,看起来像这样:
@Post()
async doStuff(@Body() dto: MyDto): Promise<string> {
// some code...
}
我使用 class-validator
以便当我的 API 收到请求时,有效负载被解析并转换为 MyDto 对象,并执行在 MyDto class 中作为注释出现的验证.请注意,MyDto 具有 class MySubDto 的嵌套对象数组。使用@ValidateNested 和@Type 注释,嵌套对象也可以正确验证。
效果很好。
现在我想为执行的验证编写测试。在我的 .spec 文件中,我写:
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);
}
这失败了,因为经过验证的 dto 对象不是 MyDto。我可以这样重写测试:
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);
}
现在可以在 MyDto 对象上正确进行验证,但不能在我的嵌套 subDto 对象上进行验证,这意味着我将不得不根据 classes 实例化我的 Dto 的 aaaall 对象,这会非常低效。此外,实例化 classes 意味着如果我自愿省略一些必需的属性或指示不正确的值,TypeScript 将引发错误。
所以问题是:
如何在我的测试中使用 NestJs 内置的请求体解析器,这样我就可以编写任何我想要的 JSON dto,解析它 as a MyDto 对象并使用 class-validator
验证函数对其进行验证?
也欢迎任何其他更好的测试验证方法!
要使用验证管道测试输入验证,我认为最好的地方是在 e2e 测试中而不是在单元测试中,只要确保你记得注册你的管道(如果你通常使用 app.useGlobalPipes()
而不是使用依赖注入)
虽然,我们应该测试我们的验证 DTO 如何与 ValidationPipe
一起工作,这是一种集成或 e2e 测试的形式。单元测试就是单元测试,对吧?!每个单元都应该可以独立测试。
Nest.js 中的 DTO 非常 单位美味。当 DTO 包含复杂的正则表达式或卫生逻辑时,有必要对 DTO 进行单元测试。
正在创建用于测试的 DTO 对象
您要查找的 Nest.js 中的请求正文解析器是 class-transformer
包。它有一个函数 plainToInstance()
可以将你的文字或 JSON 对象变成指定类型的对象。在您的示例中,指定的类型是您的 DTO 的类型:
const myDtoObject = plainToInstance(MyDto, myBodyObject)
这里,myBodyObject
是您为测试创建的普通对象,例如:
const myBodyObject = { attribute1: 1, subDto: { name: 'Vincent' } }
plainToInstance()
函数还应用您在 DTO 中的所有转换。如果你只是想测试转换,你可以在这个语句之后断言。您不必调用 validate()
函数来测试转换。
正在验证测试中的 DTO 对象
要模拟 Nest.js 的验证,只需将 myDtoObject
传递给 class-validator
包的 validate()
函数:
const errors = await validate(myDtoObject)
此外,如果您的 DTO 或 SubDTO 对象太大或太复杂而无法创建,您可以选择跳过剩余的属性或子对象,例如 subDto
:
const errors = await validate(myDtoObject, { skipMissingProperties: true })
现在您的测试对象可以没有 subDto
,例如:
const myBodyObject = { attribute1: 1 }
断言错误
除了断言 errors
数组不为空外,我还想为 DTO 中的每个验证指定一条自定义错误消息:
@IsPositive({ message: `Attribute1 must be a positive number.` })
readonly attribute1: number
自定义错误消息的一个优点是我们可以用一种用户友好的方式编写它,而不是库创建的通用消息。另一个很大的优势是我可以在我的测试中断言这个错误消息。这样我可以确定 errors
数组不为空,因为它包含此特定验证的错误而不是其他内容:
expect(stringified(errors)).toContain(`Attribute1 must be a positive number.`)
这里,stringified()
是一个简单的实用函数,将错误对象转换为 JSON 字符串,因此我们可以在其中搜索我们的错误消息:
export function stringified(errors: ValidationError[]): string {
return JSON.stringify(errors)
}
你的最终测试代码
创建一个特定于您的 DTO 的新文件,而不是 controller.spec.ts
文件,例如 my-dto.spec.ts
用于您的 DTO 的单元测试。 DTO 可以有很多单元测试,它们不应该与控制器的测试混合:
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.`)
}
请注意您不必为创建 myDtoObject
将值一一分配给属性。在大多数情况下,您的 DTO 的属性应标记为 readonly
。所以,你不能一个一个地赋值。 plainToInstance()
来救援!
就是这样!您几乎就在那里,对您的 DTO 进行单元测试。好努力!希望现在有所帮助。
TLNR: 我试图在控制器规范中测试 DTO 验证,而不是在专门为此设计的 e2e 规范中。 McDoniel 的回答为我指明了正确的方向。
我开发了一个 NestJS 入口点,看起来像这样:
@Post()
async doStuff(@Body() dto: MyDto): Promise<string> {
// some code...
}
我使用 class-validator
以便当我的 API 收到请求时,有效负载被解析并转换为 MyDto 对象,并执行在 MyDto class 中作为注释出现的验证.请注意,MyDto 具有 class MySubDto 的嵌套对象数组。使用@ValidateNested 和@Type 注释,嵌套对象也可以正确验证。
效果很好。
现在我想为执行的验证编写测试。在我的 .spec 文件中,我写:
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);
}
这失败了,因为经过验证的 dto 对象不是 MyDto。我可以这样重写测试:
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);
}
现在可以在 MyDto 对象上正确进行验证,但不能在我的嵌套 subDto 对象上进行验证,这意味着我将不得不根据 classes 实例化我的 Dto 的 aaaall 对象,这会非常低效。此外,实例化 classes 意味着如果我自愿省略一些必需的属性或指示不正确的值,TypeScript 将引发错误。
所以问题是:
如何在我的测试中使用 NestJs 内置的请求体解析器,这样我就可以编写任何我想要的 JSON dto,解析它 as a MyDto 对象并使用 class-validator
验证函数对其进行验证?
也欢迎任何其他更好的测试验证方法!
要使用验证管道测试输入验证,我认为最好的地方是在 e2e 测试中而不是在单元测试中,只要确保你记得注册你的管道(如果你通常使用 app.useGlobalPipes()
而不是使用依赖注入)
虽然,我们应该测试我们的验证 DTO 如何与 ValidationPipe
一起工作,这是一种集成或 e2e 测试的形式。单元测试就是单元测试,对吧?!每个单元都应该可以独立测试。
Nest.js 中的 DTO 非常 单位美味。当 DTO 包含复杂的正则表达式或卫生逻辑时,有必要对 DTO 进行单元测试。
正在创建用于测试的 DTO 对象
您要查找的 Nest.js 中的请求正文解析器是 class-transformer
包。它有一个函数 plainToInstance()
可以将你的文字或 JSON 对象变成指定类型的对象。在您的示例中,指定的类型是您的 DTO 的类型:
const myDtoObject = plainToInstance(MyDto, myBodyObject)
这里,myBodyObject
是您为测试创建的普通对象,例如:
const myBodyObject = { attribute1: 1, subDto: { name: 'Vincent' } }
plainToInstance()
函数还应用您在 DTO 中的所有转换。如果你只是想测试转换,你可以在这个语句之后断言。您不必调用 validate()
函数来测试转换。
正在验证测试中的 DTO 对象
要模拟 Nest.js 的验证,只需将 myDtoObject
传递给 class-validator
包的 validate()
函数:
const errors = await validate(myDtoObject)
此外,如果您的 DTO 或 SubDTO 对象太大或太复杂而无法创建,您可以选择跳过剩余的属性或子对象,例如 subDto
:
const errors = await validate(myDtoObject, { skipMissingProperties: true })
现在您的测试对象可以没有 subDto
,例如:
const myBodyObject = { attribute1: 1 }
断言错误
除了断言 errors
数组不为空外,我还想为 DTO 中的每个验证指定一条自定义错误消息:
@IsPositive({ message: `Attribute1 must be a positive number.` })
readonly attribute1: number
自定义错误消息的一个优点是我们可以用一种用户友好的方式编写它,而不是库创建的通用消息。另一个很大的优势是我可以在我的测试中断言这个错误消息。这样我可以确定 errors
数组不为空,因为它包含此特定验证的错误而不是其他内容:
expect(stringified(errors)).toContain(`Attribute1 must be a positive number.`)
这里,stringified()
是一个简单的实用函数,将错误对象转换为 JSON 字符串,因此我们可以在其中搜索我们的错误消息:
export function stringified(errors: ValidationError[]): string {
return JSON.stringify(errors)
}
你的最终测试代码
创建一个特定于您的 DTO 的新文件,而不是 controller.spec.ts
文件,例如 my-dto.spec.ts
用于您的 DTO 的单元测试。 DTO 可以有很多单元测试,它们不应该与控制器的测试混合:
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.`)
}
请注意您不必为创建 myDtoObject
将值一一分配给属性。在大多数情况下,您的 DTO 的属性应标记为 readonly
。所以,你不能一个一个地赋值。 plainToInstance()
来救援!
就是这样!您几乎就在那里,对您的 DTO 进行单元测试。好努力!希望现在有所帮助。