phpphpunitzend-formzend-framework3laminas

PHPUnit testing a Laminas form with custom validator with dependencies


I am trying to test a Laminas/Laminas-Form, that has a custom validator and this validator has a dependency that gets not injected. If I run the application in a normal environment it is working as expected. Only the test environment is affected.

As far as I can see, if I run $myForm->isValid() at some point of the ValidationChain a new PluginManager is created if not present. But this manager does not know the application configuration and assumes that my MyCustomValidatorWithDependencies can be invoked by using the InvokableFactory, which is obviously not the case. Is there a way to inject the correct application configuration into the PluginManager or just a single factory? I also checked that, in a normal environment the PluginManager is present and aware of the correct factory of my MyCustomValidatorWithDependencies before and during $myForm->isValid() is executed.

<?php

// AppTest\Form\MyFormTest
class MyFormTest extends TestCase
{
    public function testIsValid(): void
    {
        $myForm = new MyForm();
        $myForm->setData($data);

        $makeAssertionForIsValid = $myForm->isValid();
        $makeAssertionForMessages = $myForm->getMessages();
    }
}

// App\Form\MyForm
class MyForm extends Form implements InputFilterProviderInterface {

    public function __construct() {
        parent::__construct('myFormName');
        $this->setInputFilter(new InputFilter());
    }

    public function getInputFilterSpecification(): array
    {
        return [
            'myValue' => [
                'validators' => [
                    [
                        'name' => MyCustomValidatorWithDependencies::class,
                    ],
                ],
            ],
        ];
    }
}

// App\Validator\MyCustomValidatorWithDependencies
class MyCustomValidatorWithDependencies extends AbstractValidator
{
    public function __construct(
        MyCustomDependency $myCustomDependency,
        $options = []
    ) {
        $this->myCustomDependency = $myCustomDependency;
        parent::__construct($options);
    }


    public function isValid($value) {
        // do validation...
    }
}

// App\Validator\Factory\MyCustomValidatorWithDependenciesFactory
class MyCustomValidatorWithDependenciesFactory implements FactoryInterface {
    public function __invoke(
        ContainerInterface $container,
        $requestedName,
        array $options = null
    ) {
        return new MyCustomValidatorWithDependencies(
            $container->get(MyCustomDependency::class),
            $options,
        );
    }
}


// App\config\module.config.php
return [
    'service_manager' => [
        'factories' => [
            App\Validator\MyCustomValidatorWithDependencies::class => App\Validator\Factory\MyCustomValidatorWithDependenciesFactory::class,
            App\Dependency\MyCustomDependency::class => App\Dependency\Factory\MyCustomDependencyFactory::class,
        ],
    ],
    'validators' => [
        'factories' => [
            App\Validator\MyCustomValidatorWithDependencies::class => App\Validator\Factory\MyCustomValidatorWithDependenciesFactory::class,
        ],
    ],
];

Solution

  • I just missed one line.

    $formManager = $container->get('FormElementManager');
    $myForm = $formManager->get(MyForm::class);
    

    I changed my test case to the following and it is working as I expect.

    class MyFormTest extends TestCase
    {
        use ApplicationTestTrait;
    
        public function testIsValid(): void
        {
            $container = $this->getApplicationServiceLocator();
    
            // create a mock
            $myCustomDependency = $this->createStub(MyCustomDependency::class);
    
            // overwrite the factory in the application service locator to return
            // the mock instead of the real dependency.
            $container->setFactory(
                MyCustomDependency::class,
                static function () use
                (
                    $myCustomDependency
                ) {
                    return $myCustomDependency;
                }
            );
    
            // get the form
            $formManager = $container->get('FormElementManager');
            $myForm = $formManager->get(MyForm::class);
            $myForm->setData($data);
    
            $makeAssertionForIsValid = $myForm->isValid();
            $makeAssertionForMessages = $myForm->getMessages();
        }
    }
    

    In order to use dependency injection we need to spin up the application. To prevent duplicated code in multiple test cases, I used a *TestTrait and insert that trait in all test cases if needed.

    trait ApplicationTestTrait {
        
        protected null|ApplicationInterface $application;
    
        public function getApplication(): ApplicationInterface
        {
            if ($this->application) {
                return $this->application;
            }
    
            $this->application = Application::init(
                include __DIR__ . '/path/to/config/application.config.php')
            );
            return $this->application;
        }
    
    
        public function getApplicationServiceLocator(): ServiceLocatorInterface
        {
            return $this->getApplication()
                ->getServiceManager();
        }
    }
    

    For more information please also have a look at this discussion: https://discourse.laminas.dev/t/phpunit-testing-a-laminas-form-with-custom-validator-with-dependencies/2010
    and the official docs at https://docs.laminas.dev/laminas-form/application-integration/usage-in-a-laminas-mvc-application/#create-factory-for-controller