如何去单元测试你的表单

3.4 版本
维护中的版本

表单组件包含三个核心对象:表单类型(实现FormTypeInterface),FormFormView

经常被程序员操作的唯一的类是表单类型类,这个类作为表单的基类。它被用来生成 Form 以及 FormView。你可以通过模拟它和工厂的交互作用来直接测试它,但是这个会很复杂。最好的办法就是将它传递到 FormFactory 这样就会像真正在应用程序中使用一样。这是简单的引导,你可以信任 Symfony 组件测试,因为这个足够用了。

这里已经有了一个类你可以直接用它来进行简单的 FormType 测试:TypeTestCase。它是用来测试核心类型的,同时你也可以用它测试你的类型。

根据你安装Symfony的不同方式,symfony表单测试组件有可能没有被下载。如果有这种情况,你可以使用Composer的--prefer-source选项。

基础知识 

最简单的TypeTestCase实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// tests/AppBundle/Form/Type/TestedTypeTest.php
namespace Tests\AppBundle\Form\Type;
 
use AppBundle\Form\Type\TestedType;
use AppBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;
 
class TestedTypeTest extends TypeTestCase
{
    public function testSubmitValidData()
    {
        $formData = array(
            'test' => 'test',
            'test2' => 'test2',
        );
 
        $form = $this->factory->create(TestedType::class);
 
        $object = TestObject::fromArray($formData);
 
        // submit the data to the form directly
        $form->submit($formData);
 
        $this->assertTrue($form->isSynchronized());
        $this->assertEquals($object, $form->getData());
 
        $view = $form->createView();
        $children = $view->children;
 
        foreach (array_keys($formData) as $key) {
            $this->assertArrayHasKey($key, $children);
        }
    }
}

那么,他是如何测试呢?来一个详细的解释吧。

首先核实你的FormType是否编译。它包含核对基本的类继承,buildForm函数以及各种配置。这应该是你写的第一句测试:

1
$form = $this->factory->create(TestedType::class);

下面这个测试检查了你的表单使用的数据转换器( data transformers )有没有失败的。如果测试一个数据转换器会抛出一个异常,那么isSynchronized()方法就会返回fasle

1
2
$form->submit($formData);
$this->assertTrue($form->isSynchronized());

不测试这个验证:他是监听器实现的,但在这个测试案例中他是不活跃的并且他依赖验证配置。替代,直接对你的自定义约束进行测试。

下面来,测试表单提交和映射。下列的测试检查了所有的字段是否正确被指定:

1
$this->assertEquals($object, $form->getData());

最后,检查创建的FormView。你应当检查是否你想要在子属性上展示所有的控件:

1
2
3
4
5
6
$view = $form->createView();
$children = $view->children;
 
foreach (array_keys($formData) as $key) {
    $this->assertArrayHasKey($key, $children);
}

从服务容器测试类型 

你的表单可能会被作为一个服务,因为他依赖于其他的服务(例如,Doctrine entity manager)。在这个案例中,使用上面的代码就无法正常工作了,因为这个表单组件仅仅实例化表单类型并没有传入任何的参数到构造函数。

为了解决这个问题,你必须模拟注入的依赖关系,实例化我们的表单类型并使用PreloadedExtension去确保FormRegistry 使用创建的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// tests/AppBundle/Form/Type/TestedTypeTests.php
namespace Tests\AppBundle\Form\Type;
 
use Symfony\Component\Form\PreloadedExtension;
// ...
 
class TestedTypeTest extends TypeTestCase
{
    private $entityManager;
 
    protected function setUp()
    {
        // mock any dependencies
        // 模仿任何的依赖
        $this->entityManager = $this->getMock('Doctrine\Common\Persistence\ObjectManager');
 
        parent::setUp();
    }
 
    protected function getExtensions()
    {
        // create a type instance with the mocked dependencies
        // 创建一个传入模仿依赖的类型实例
        $type = new TestedType($this->entityManager);
 
        return array(
            // register the type instances with the PreloadedExtension
            // 使用PreloadedExtension去注册类型实例
            new PreloadedExtension(array($type), array()),
        );
    }
 
    public function testSubmitValidData()
    {
        // Instead of creating a new instance, the one created in
        // getExtensions() will be used.
        // 不是创建一个新实例,是在getExtensions()创建的一个实例将被使用
        $form = $this->factory->create(TestedType::class);
 
        // ... your test
    }
}

添加自定义扩展 

他经常发生,因为你使用表单扩展时会添加一些配置选项。其中一种情况就是ValidatorExtensioninvalid_message选项。这个TypeTestCase只加载核心的表单扩展,这意味着如果你尝试去测试一个类,这个类依赖于其他的扩展,那么一个InvalidOptionsException可能会出现。这个getExtensions()方法允许你去返回一个扩展列表并去注册他们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// tests/AppBundle/Form/Type/TestedTypeTests.php
namespace Tests\AppBundle\Form\Type;
 
// ...
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\ConstraintViolationList;
 
class TestedTypeTest extends TypeTestCase
{
    private $validator;
 
    protected function getExtensions()
    {
        $this->validator = $this->getMock(
            'Symfony\Component\Validator\Validator\ValidatorInterface'
        );
        $this->validator
            ->method('validate')
            ->will($this->returnValue(new ConstraintViolationList()));
 
        return array(
            new ValidatorExtension($this->validator),
        );
    }
 
    // ... your tests
}

不同数据集的测试 

如果你不熟悉 PHPUnitdata providers,这可能是使用它们的好机会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// tests/AppBundle/Form/Type/TestedTypeTests.php
namespace Tests\AppBundle\Form\Type;
 
use AppBundle\Form\Type\TestedType;
use AppBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;
 
class TestedTypeTest extends TypeTestCase
{
    /**
     * @dataProvider getValidTestData
     */
    public function testForm($data)
    {
        // ... your test
    }
 
    public function getValidTestData()
    {
        return array(
            array(
                'data' => array(
                    'test' => 'test',
                    'test2' => 'test2',
                ),
            ),
            array(
                'data' => array(),
            ),
            array(
                'data' => array(
                    'test' => null,
                    'test2' => null,
                ),
            ),
        );
    }
}

上面的代码将以3个不同的数据集来运行三次测试。他已经考虑到从测试中解耦测试fixtures并很容易的测试多组数据。

你也可以传入其他的参数,比如一个布尔值,如果表单必须被同步这个给定的数据或者不同步等。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)