如何使用 NestJS 和 class-validator 手动测试输入验证

Posted

技术标签:

【中文标题】如何使用 NestJS 和 class-validator 手动测试输入验证【英文标题】:How to manually test input validation with NestJS and class-validator 【发布时间】:2020-03-09 14:25:02 【问题描述】:

TLNR:我试图在控制器规范中测试 DTO 验证,而不是在 e2e 规范中进行测试,而 e2e 规范正是为此而设计的。 McDoniel 的回答为我指明了正确的方向。


我开发了一个 NestJS 入口点,如下所示:

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

我使用class-validator,这样当我的 API 收到请求时,有效负载会被解析并转换为 MyDto 对象,并执行在 MyDto 类中作为注释出现的验证。请注意,MyDto 有一个 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 对象上进行验证,这意味着我必须使用相应的类来实例化我的 Dto 的 aaaall 对象,这将是非常低效的。此外,实例化类意味着如果我自愿省略一些必需的属性或指示不正确的值,TypeScript 将引发错误。

所以问题是:

如何在我的测试中使用 NestJs 内置的请求正文解析器,以便我可以为 dto 编写任何我想要的 JSON,将其解析为 作为 MyDto 对象并使用 class-validator 验证它验证函数?

也欢迎任何替代的更好的实践方法来测试验证!

【问题讨论】:

【参考方案1】:

要使用验证管道测试输入验证,我认为最好的地方是在 e2e 测试中而不是在单元测试中,只要确保你记得注册你的管道(如果你通常使用 @ 987654321@而不是使用依赖注入)

【讨论】:

如何使用注册管道方法? 如前所述,如果您通常在 main.ts 中使用 app.useGlobalPipes(),则需要在 e2e 测试中执行相同操作,因为 RootTestModule 不会通过 @ 987654326@(也不应该,因为它是完全不同的上下文)【参考方案2】:

虽然,我们应该测试我们的验证 DTO 如何与 ValidationPipe 一起工作,这是一种集成或 e2e 测试形式。单元测试就是单元测试,对吧?!每个单元都应该是可独立测试的。

Nest.js 中的 DTO 是完美的unit-tastable。当 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 文件,例如用于 DTO 单元测试的 my-dto.spec.ts。 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 进行单元测试。好努力!希望现在有所帮助。

【讨论】:

感谢您的完整回复!我在这里学到了一些有用的东西。现在,这给我留下了另外两个问题。 A/ 使用这种方法,您如何另外断言控制器将实际应用验证? B/ 我的一些控制器正在做一些额外的验证(整个逻辑涉及 DTO 内容的精确和特定性质(例如:如果 attribute1=3,subDto 名称不能是 'Vincent'),无法通过您建议的 dto-unit-test 方式进行测试,但可以使用 e2e 方法进行测试。有什么想法吗? @Bob, A/ Controller 与 DTO 的交互方式应该在 e2e 或集成测试中进行测试,因为它是 DTO 和 Controller 之间的集成。只需在 Supertest 中传递无效输入并断言它会引发您指定的错误。 @Bob,B/ 听起来像是您的业务逻辑。检查某些输入是否满足您的业务需求,应在您的服务内部进行分析和验证,并从那里抛出错误。控制器应该保持沉默。它们不应包含任何逻辑或验证。他们的主要工作是路由和解析输入。只需将请求从控制器转发到包含所有业务相关验证和其他逻辑的服务。 DTO 验证应仅包括通用验证,例如用户名最小长度、最大长度或诸如从输入字段中修剪空格等卫生措施。 这是有道理的。感谢您的提示和您的时间!

以上是关于如何使用 NestJS 和 class-validator 手动测试输入验证的主要内容,如果未能解决你的问题,请参考以下文章

在 NestJS 中抛出与 `class-validator` 相同的错误格式

在 TypeScript 中使用 `class-validator` 确认密码

使用 class-validator 和 Nest.js 验证对象数组

typescript.-如何使用类验证器和类转换器(Nestjs)验证子类中的特定字段

Class-validator - 验证对象数组

Nestjs IsEnum dto 验证和招摇