Пользовательское ограничение проверки модульного тестирования в Symfony 2.1, но без доступа к контейнеру?

Как я могу выполнить модульное тестирование ContainsItalianVatinValidator пользовательского валидатора, но без *без доступа к контейнеру* и validator сервису (и, таким образом, создать объект-заглушку)?

class ContainsItalianVatinValidator extends ConstraintValidator
{
    /**
     * @param mixed $value
     * @param \Symfony\Component\Validator\Constraint $constraint
     */
    public function validate($value, Constraint $constraint)
    {    
        if (!preg_match('/^[0-9]{11}$/', $value, $matches)) {
            $this->context->addViolation($constraint->message, array(
                '%string%' => $value
            ));
        }

        // Compute and check control code
        // ...
    }
}

В моем тестовом примере я знаю, что должен получить доступ к ConstraintViolationList, но я не знаю, как это сделать из самого валидатора:

class ContainsItalianVatinValidatorTest extends \PHPUnit_Framework_TestCase
{
    public function testEmptyItalianVatin()
    {
        $emptyVatin = '';
        $validator  = new ContainsItalianVatinValidator();
        $constraint = new ContainsItalianVatinConstraint();

        // Do the validation
        $validator->validate($emptyVatin, $constraint);

        // How can a get a violation list and call ->count()?
        $violations = /* ... */;

        // Assert
        $this->assertGreaterThan(0, $violations->count());
    }
}

person gremo    schedule 12.09.2012    source источник
comment
Я бы извлек логику проверки в службу и написал модульный тест для этой службы. Внутри класса валидатора вы проверяете свое ограничение с помощью службы и добавляете сообщение, если проверка не удалась. Таким образом, ваша логика проверки не привязана к фреймворку и более устойчива к будущим изменениям.   -  person fabwu    schedule 24.02.2017


Ответы (3)


Когда вы посмотрите на родительский класс валидатора 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.

На самом деле, этот вопрос больше относится к PHPUnit, чем к Symfony. Вам может быть интересна глава Test Doubles документации PHPUnit.

person Florian Eckerstorfer    schedule 12.09.2012
comment
Удивительно хорошее объяснение. Единственное, чего я не могу понять, так это почему, по вашему мнению, неправильно подсчитывать нарушения и почему я предпочитаю полагаться на само сообщение об ограничении. В любом случае, +1. - person gremo; 13.09.2012
comment
Зачем считать нарушения. По крайней мере, в коде вашего вопроса есть только один вызов addViolation. Если этот метод вызывается один раз, к контексту будет добавлено ровно одно нарушение (модульные тесты Symfony2 проверяют это). - person Florian Eckerstorfer; 13.09.2012
comment
Если в коде должно быть больше вызовов addViolation, вы можете добавить несколько операторов $context->expects, каждый из которых охватывает один отдельный вызов addViolation. К сожалению, PHPUnit предлагает только два метода для подсчета количества вызовов метода once и any. Однако Mockery — это фиктивная библиотека, совместимая с PHPUnit и способная подсчитывать количество вызовов методов для фиктивный объект. - person Florian Eckerstorfer; 13.09.2012
comment
Спасибо, я проверю вызовы addViolation. - person gremo; 13.09.2012
comment
С 2016 года PHPUnit также поддерживает $this->exactly($numberOfCalls) для подсчета вызовов фиктивного метода. - person mblaettermann; 13.02.2016

Обновлено для 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);
    }
}
person iisisrael    schedule 03.09.2015
comment
Какой отличный ответ! Большое тебе спасибо! - person cezar; 24.06.2016

Обновлено для 3.4:

Я поместил создание контекста в трейт, чтобы мы могли повторно использовать его для всех наших пользовательских ограничений.

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;
    }
}
person winkbrace    schedule 08.11.2018