Symfony 2.1中的单元测试自定义验证约束,但不访问容器?

Symfony 2.1中的单元测试自定义验证约束,但不访问容器?,symfony,phpunit,symfony-2.1,Symfony,Phpunit,Symfony 2.1,如何对containistalianvatinvalidator自定义验证器进行单元测试,而不访问容器*和validator服务(从而创建存根对象) 类ContainsItalianVatinValidator扩展了ConstraintValidator { /** *@param混合$value *@param\Symfony\Component\Validator\Constraint$Constraint */ 公共函数验证($value,Constraint$Constraint) {

如何对
containistalianvatinvalidator
自定义验证器进行单元测试,而不访问容器*和
validator
服务(从而创建存根对象)

类ContainsItalianVatinValidator扩展了ConstraintValidator
{
/**
*@param混合$value
*@param\Symfony\Component\Validator\Constraint$Constraint
*/
公共函数验证($value,Constraint$Constraint)
{    
如果(!preg_match('/^[0-9]{11}$/',$value,$matches)){
$this->context->addViolation($constraint->消息,数组)(
“%string%”=>$value
));
}
//计算并检查控制代码
// ...
}
}
在我的测试用例中,我知道我应该访问,但我不知道如何从验证器本身访问:

class ContainsItalianVatinValidatorTest扩展\PHPUnit\u框架\u测试用例
{
公共功能测试mptyalianvatin()
{
$emptyVatin='';
$validator=新的ContainesTalianVatinValidator();
$constraint=新的CONTAINSTALIANVATINCONSTRAINT();
//进行验证
$validator->validate($emptyVatin,$constraint);
//如何获取冲突列表并调用->count()?
$violations=/*…*/;
//断言
$this->assertgreater大于(0,$influences->count());
}
}

当您查看验证器的父类
Symfony\Component\validator\ConstraintValidator
时,您会看到有一个名为
initialize
的方法,它以
Symfony\Component\validator\ExecutionContext
的实例作为参数

创建验证器后,可以调用
initialize
方法,并将模拟上下文传递给验证器。您不必测试
addViolation
方法是否正常工作,只需测试是否调用了它以及是否使用正确的参数调用了它。您可以使用PHPUnit的模拟功能来实现这一点

...
$validator  = new ContainsItalianVatinValidator();
$context = $this->getMockBuilder('Symfony\Component\Validator\ExecutionContext')-> disableOriginalConstructor()->getMock();

$context->expects($this->once())
    ->method('addViolation')
    ->with($this->equalTo('[message]'), $this->equalTo(array('%string%', '')));

$validator->initialize($context);

$validator->validate($emptyVatin, $constraint);
...
在此代码中,您必须用存储在
$constraint->message
中的消息替换[message]


事实上,这个问题与PHPUnit比Symfony更相关。您可能会发现PHPUnit文档中的章节很有趣。

针对Symfony 2.5+进行了更新。为验证器中的
validate()
方法可能添加的每个可能的消息添加一个测试,并添加触发该消息的值

<?php

namespace AcmeBundle\Tests\Validator\Constraints;

use AcmeBundle\Validator\Constraints\SomeConstraint;
use AcmeBundle\Validator\Constraints\SomeConstraintValidator;

/**
 * Exercises SomeConstraintValidator.
 */
class SomeConstraintValidatorTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Configure a SomeConstraintValidator.
     *
     * @param string $expectedMessage The expected message on a validation violation, if any.
     *
     * @return AcmeBundle\Validator\Constraints\SomeConstraintValidator
     */
    public function configureValidator($expectedMessage = null)
    {
        // mock the violation builder
        $builder = $this->getMockBuilder('Symfony\Component\Validator\Violation\ConstraintViolationBuilder')
            ->disableOriginalConstructor()
            ->setMethods(array('addViolation'))
            ->getMock()
        ;

        // mock the validator context
        $context = $this->getMockBuilder('Symfony\Component\Validator\Context\ExecutionContext')
            ->disableOriginalConstructor()
            ->setMethods(array('buildViolation'))
            ->getMock()
        ;

        if ($expectedMessage) {
            $builder->expects($this->once())
                ->method('addViolation')
            ;

            $context->expects($this->once())
                ->method('buildViolation')
                ->with($this->equalTo($expectedMessage))
                ->will($this->returnValue($builder))
            ;
        }
        else {
            $context->expects($this->never())
                ->method('buildViolation')
            ;
        }

        // initialize the validator with the mocked context
        $validator = new SomeConstraintValidator();
        $validator->initialize($context);

        // return the SomeConstraintValidator
        return $validator;
    }

    /**
     * Verify a constraint message is triggered when value is invalid.
     */
    public function testValidateOnInvalid()
    {
        $constraint = new SomeConstraint();
        $validator = $this->configureValidator($constraint->someInvalidMessage);

        $validator->validate('someInvalidValue', $constraint);
    }

    /**
     * Verify no constraint message is triggered when value is valid.
     */
    public function testValidateOnValid()
    {
        $constraint = new SomeConstraint();
        $validator = $this->configureValidator();

        $validator->validate('someValidValue', $constraint);
    }
}
为3.4更新:

我将上下文创建放在一个trait中,这样我们就可以在所有自定义约束中重用它

class SomeConstraintValidatorTest extends TestCase
{
    use ConstraintValidationTrait;

    /** @var SomeConstraint */
    private $constraint;

    protected function setUp()
    {
        parent::setUp();

        $this->constraint = new SomeConstraint();
    }

    public function testValidateOnInvalid()
    {
        $this->assertConstraintRejects('someInvalidValue', $this->constraint);
    }

    public function testValidateOnValid()
    {
        $this->assertConstraintValidates('someValidValue', $this->constraint);
    }
}
特点:

<?php

use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContext;

trait ConstraintValidationTrait
{
    /**
     * The assertion is done in the mock.
     *
     * @param mixed $value
     */
    public function assertConstraintValidates($value, Constraint $constraint): void
    {
        $validator = $this->createValidator($constraint, true);
        $validator->validate($value, $constraint);
    }

    /**
     * The assertion is done in the mock.
     *
     * @param mixed $value
     */
    public function assertConstraintRejects($value, Constraint $constraint): void
    {
        $validator = $this->createValidator($constraint, false);
        $validator->validate($value, $constraint);
    }

    /** This is the phpunit mock method this trait requires */
    abstract protected function createMock($originalClassName): MockObject;

    private function createValidator(Constraint $constraint, bool $shouldValidate): ConstraintValidator
    {
        $context = $this->mockExecutionContext($shouldValidate);

        $validatorClass = get_class($constraint) . 'Validator';

        /** @var ConstraintValidator $validator */
        $validator = new $validatorClass();
        $validator->initialize($context);

        return $validator;
    }

    /**
     * Configure a SomeConstraintValidator.
     *
     * @param string|null $expectedMessage The expected message on a validation violation, if any.
     *
     * @return ExecutionContext
     */
    private function mockExecutionContext(bool $shouldValidate): ExecutionContext
    {
        /** @var ExecutionContext|MockObject $context */
        $context = $this->createMock(ExecutionContext::class);

        if ($shouldValidate) {
            $context->expects($this->never())->method('addViolation');
        } else {
            $context->expects($this->once())->method('addViolation');
        }

        return $context;
    }
}

非常好的解释。我唯一不能理解的是,在你看来,为什么计算违规是错误的,为什么我更喜欢依赖约束消息本身。不管怎样,+1.你为什么要计算违规行为。至少在您问题中的代码中,只有一个调用
addinvalition
。如果该方法被调用一次,则会向上下文中添加一个冲突(Symfony2的单元测试会测试该冲突)。如果代码中应该有更多对
addinvalition
的调用,则可以添加多个
$context->expects
语句,其中每个语句都包含一个对
addinvalition
的不同调用。遗憾的是,PHPUnit只提供了两种方法来计算一个方法的调用次数
一次
任何
。但是,它是一个与PHPUnit兼容的模拟库,可以计算模拟对象上的方法调用数。谢谢,我将检查对addViolation的调用。从2016年起,PHPUnit还支持
$this->justice($numberOfCalls)
来计算对模拟方法的调用数。这是一个非常好的答案!非常感谢你!我将把验证逻辑提取到一个服务中,并为此服务编写一个单元测试。在validator类中,检查服务的约束,如果验证失败,则添加一条消息。因此,您的验证逻辑不会与框架耦合,并且对未来的更改更加健壮。