如何对Symfony验证约束进行单元测试?

时间:2017-02-22 17:20:09

标签: php unit-testing validation symfony

验证类是否具有所有预期约束(属性约束,get / is / has方法约束,自定义回调约束,类约束)的最佳方法是什么?

好方法将是一种方式:

  • 允许定义预期在给定类上设置的约束能够识别缺失的
  • 不需要运行实际验证代码,因为此代码已经过验证器类测试
  • 有合理的“安排”步骤(例如setUp方法),这对于普通测试用户来说相当容易准备和理解

我们已经尝试了不同的解决方案(参见代码示例中的1.和2.)但它们都没有真正解决这个问题。

验证元数据的想法(参见代码示例中的3.)似乎很有希望,但也许有人可以提出更好的建议吗?

用例

提供一些真实的用法:

  1. 有一个Foo实体类和一个Bar实体类,可以扩展Foo

    有人从NotNull的某个字段中删除Foo约束(之前更新Foo的单元测试)。

    我们希望确保我们拥有一套单元测试,以检测Bar对其字段的NotNull不再有class MyEntityValidationTest extends \PHPUnit_Framework_TestCase { public function testMyEntityIsInvalidBecauseOfSomething() { $containerProphecy = $this->prophesize(ContainerInterface::class); $validator = Validation::createValidatorBuilder() ->enableAnnotationMapping() ->setConstraintValidatorFactory(new ConstraintValidatorFactory($containerProphecy->reveal(), [])) ->getValidator(); $myEntity = new MyEntity(); $violations = $validator->validate($myEntity); $this->assertCount(1, $violations); } public function testMyEntityIsInvalidBecauseOfSomethingElse() { // if you use validator.constraint_validator tag with an alias for your validator service (the old way), // you also have to set up get() method call on container mock and define mapping $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->get('my_entity_validator_service')->willReturn(new MyEntityValidator()); $validators = [ 'my_entity_validator_service' => 'validator.my_entity', ]; $validator = Validation::createValidatorBuilder() ->enableAnnotationMapping() ->setConstraintValidatorFactory(new ConstraintValidatorFactory($containerProphecy->reveal(), $validators)) ->getValidator(); $myEntity = new MyEntity(); $violations = $validator->validate($myEntity); $this->assertCount(1, $violations); } } 约束。

  2. 假设有一个验证器可以实现非常复杂的验证逻辑,例如: “给定的日期必须是在该月的第一个星期一,除非它是六月或十月,然后它必须是上周二,但那么...... ”(你知道我的意思)。 /> 此验证器已经过完全单元测试,我们知道它可以正常工作。

    我们还假设有100个实体类在某些字段上设置了此约束。

    将对象设置为测试的过程很复杂(它已在验证器单元测试中完成)。我们不想做100倍以上 如果有一天该验证的要求发生变化,例如“星期二”条件必须扩展到“星期二或星期三,但只有...... ”,那么我们不应该被迫更改101个单元测试而只需要一个 - 验证员类的单元测试。

  3. 代码示例

    以下是上面提到的代码段:

    1. 通过使用验证器服务实际运行验证过程并计算创建的违规来测试约束。

      validator->initialize()

      此解决方案的主要问题是:

      • 它测试验证器的实际实现,它应该已经在验证器类测试中测试过(所以从技术上来说它不是单元测试);
      • 设置步骤,即创建验证器,定义容器模拟和验证器映射,可能非常复杂,使测试难以阅读。
    2. 通过模拟传递给class MyEntityValidationTest extends \PHPUnit_Framework_TestCase { public function testMyEntityIsInvalidBecauseOfSomething() { $myEntity = $this->prophesize(MyEntity::class); $myEntity ->isStillValid() ->willReturn(false); $constraintProphecy = $this->prophesize(MyEntityConstraint::class); $validator = new MyEntityValidator(); $validator->initialize($this->buildContextWithExpectedViolation()); $validator->validate($myEntity->reveal(), $constraintProphecy->reveal()); } private function buildContextWithExpectedViolation() { $violationBuilderProphecy = $this->prophesize(ConstraintViolationBuilder::class); $violationBuilderProphecy ->atPath('configurationId') ->shouldBeCalled() ->willReturn($violationBuilderProphecy); // if you want to use translation messages with parameters you need to configure it as well $violationBuilderProphecy ->setParameter('%configuration_id%', Argument::any()) ->shouldBeCalled() ->willReturn($violationBuilderProphecy); $violationBuilderProphecy ->addViolation() ->shouldBeCalled(); $contextProphecy = $this->prophesize(ExecutionContextInterface::class); $contextProphecy ->buildViolation('error message') ->willReturn($violationBuilderProphecy->reveal()); return $contextProphecy->reveal(); } } 方法的上下文对象并设置进行测试所需的所有期望来测试约束。

      TestCase

      同样,这个解决方案的问题是设置步骤,即准备所有模拟和定义期望对于创建者和测试的读者来说都是非常困难的。

      Symfony(技术上,Validator组件)提供了一个帮助器class MyEntityValidationTest extends \PHPUnit_Framework_TestCase { public function testMyEntityHasExpectedValidationConstraintsAttached() { $classMetadata = Validation::createValidatorBuilder() ->enableAnnotationMapping() ->getValidator() ->getMetadataFor(new MyEntity()); $this->assertClassContainsConstraint($classMetadata, FooConstraint::class); $this->assertClassContainsConstraint($classMetadata, BarConstraint::class, [ 'thisField' => 123, 'thatField' => 'test', ]); $namePropertyMetadata = $classMetadata->getPropertyMetadata('name'); $this->assertPropertyContainsConstraint($namePropertyMetadata, NotBlank::class); $valuePropertyMetadata = $classMetadata->getPropertyMetadata('value'); $this->assertPropertyContainsConstraint($valuePropertyMetadata, GreaterThan::class, [ 'value' => 100, ]); } private function assertClassContainsConstraint( MetadataInterface $classMetadata, string $constraintClass, array $constraintFields = [] ) { // $this->fail() if none of constrains in $classMetadata // matches $constraintClass / $constraintFields } private function assertPropertyContainsConstraint( PropertyMetadataInterface $propertyMetadata, string $constraintClass, array $constraintFields = [] ) { // $this->fail() if none of constrains in $propertyMetadata // matches $constraintClass / $constraintFields } } 类(Symfony\Component\Validator\Test\ConstraintValidatorTestCase),可以用来使这样的写入测试更容易一些,但它仍然不完美。

    3. 通过准备验证器服务来测试约束,获取为要测试的类生成的元数据信息,并验证是否已定义所有预期约束。

      assertClassContainsConstraint()

      这需要一些额外的工作来实现assertPropertyContainsConstraint()TestCase或类似的方法,但一旦完成(例如在抽象assertSomething()类中),它可以在所有其他测试类中重用。< / p>

      此外,这具有处理以不同格式定义的约束的附加益处,例如,注释,yaml,xml,php。

      请注意,这使用PHPUnit的{{1}}约定。能够使用phpspec的“specking”方法,能够告诉在准备验证服务时预期会发生什么,这样会更好。

0 个答案:

没有答案