如何真正单元测试Symfony表单?

时间:2018-01-03 17:24:54

标签: php symfony unit-testing mocking phpunit

以下示例来自official documentation

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);
        }
    }
}

但是,使用真正的单元测试方法,测试应该只包含一个单独的类SUT,其他一切应该是Test Doubles,如存根,模拟对象......

我们应该如何使用像模拟对象这样的测试双打单元测试Symfony表单?

我们可以假设一个简单的表单类:

class TestedType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstname', TextType::class, [
                'label' => 'First name',
                'attr' => [
                    'placeholder' => 'John Doe',
                ],
            ])
    }
}

1 个答案:

答案 0 :(得分:1)

您的问题很旧,但是没有得到答案,我最近偶然遇到同样的问题时偶然发现了它。

  

我们应该如何使用模拟对象之类的Test Doubles对Symfony表单进行单元测试?

我们通常测试单个方法的结果,但是对于 buildForm 方法,唯一可以使用简单表单类型编写的测试是 interaction FormBuilderInterface 实例作为参数传递。

// Imports and PhpDoc annotations skipped for brievety
public class TestedTypeTest  extends TestCase
{
    private $systemUnderTest;

    protected function setUp()
    {
        parent::setUp();
        $this->systemUnderTest = new TestedType();
    }

    /**
     * Tests that form is correctly build according to specs
     */
    public function testBuildForm(): void
    {
        $formBuilderMock = $this->createMock(FormBuilderInterface::class);
        $formBuilderMock->expects($this->atLeastOnce())->method('add')->willReturnSelf();

        // Passing the mock as a parameter and an empty array as options as I don't test its use
        $this->systemUnderTest->buildForm($formBuilderMock, []);
    }
}

正确地说,这是一个相当基本的单元测试,它是对您的类进行单独测试。

别犯同样的错误,不要忘记调用 willReturnSelf 方法,因为 add 是一个 chainable方法({ {3}}。

您可以从此处开始并完善您的测试,将其与实施紧密结合起来

// Tests number of calls to add method, in my case, 2
$formBuilderMock->expects($this->exactly(2))->method('add')->willReturnSelf();

// Tests number of calls AND parameters successively passed
$formBuilderMock->expects($this->exactly(2))->method('add')->withConsecutive(
    [$this->equalTo('field_1'), $this->equalTo(TextType::class)],
    [$this->equalTo('field_2'), $this->equalTo(TextType::class)]
);

我想您明白了……在那里,您的测试与实现细节相关联,迫使您在更改代码后立即对其进行更改:根据您的上下文,它是您更改测试粒度的调用


边注

由于您没有指定Symfony和PhpUnit的任何版本,请知道我的示例使用以下版本:

  • Symfony 4.1
  • PhpUnit 7.5

在回答有关测试行为时阅读的问题

fluent interface pattern
Do mocks break the "test the interface, not the implementation" mantra?