Tested Image Preview... Sort Of :)


We got a lot done in the previous video. We now have a working Wallpaper Create, and Update facility.

It would be good to get the extra pieces from our untested approach added in to our tested setup. Therefore next up we will tackle the Wallpaper Image Preview on Update feature.

There are a bunch of ways we could go about testing this - after all, it is a visual 'thing', so do we test that the thing we see is behaving as expected, or that the code that makes this all work behaves properly? Or do we test both?

My opinion here is to focus on unit tests (think: PhpSpec) for as long as possible, avoiding testing the visual side of things if at all possible. Rather than refer to these sorts of tests as "visual tests", I will switch to calling them "Acceptance Tests", which is generally what these sorts of tests fall under.

  • Acceptance testing is slow
  • Acceptance testing is prone to failure

Acceptance tests are slow because they require interacting with your site as though a real visitor were "driving the mouse". This is typically done with a tool like Selenium, and whilst nice to have and fun to watch, they do take a while to run (minutes usually, rather than seconds).

A bigger concern for me is that Acceptance Tests tend to throw up a lot of false positives. In other words, they fail for unexpected reasons - maybe Selenium died mid run, maybe someone changed a class name for a button from btn to button or similar, or maybe Selenium just couldn't find the element on this run (trust me, it happens!). Acceptance tests tend to focus too rigidly on on-page specifics (like CSS class names, etc) rather than overall site goals.

True acceptance testing requires a wider discussion than this - there are ways around using CSS class names and specific HTML selectors, for example.

Now, this is a can of worms I do not want to open the lid on - at least no further than this.

Instead, we will focus on using PhpSpec as far as possible, which will unfortunately leave some parts of this feature without a test. This will predominantly relate to the view, as we shall see in the next video.

From our untested approach we know that we will need something similar to following form type:

<?php

namespace AppBundle\Form\Type;

use AppBundle\Entity\Wallpaper;
use AppBundle\Form\DataTransformer\FileTransformer;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CustomFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class)
        ;

        $builder
            ->addViewTransformer(
                new FileTransformer(
                    $this->logger
                )
            )
        ;
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        /**
         * @var $entity Wallpaper
         */
        $entity = $form->getParent()->getData();

        if ($entity) {
            $view->vars['file_uri'] = (null === $entity->getFilename())
                ? null
                : '/images/' . $entity->getFilename()
            ;
        }
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'file_uri' => null,
        ]);
    }
}

We have a variation of this which we created already:

<?php

namespace AppBundle\Form\Type;

use AppBundle\File\SymfonyUploadedFile;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UploadedFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'multiple' => false,
            ])
        ;
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['full_name'] = $view->vars['full_name'] . '[file]';
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefaults([
                'data_class' => SymfonyUploadedFile::class
            ])
        ;
    }
}

There are differences here which we have covered already in this series. If unsure at this point, please go back through the untested and tested approaches - or leave a comment below and I will do my best to clarify further. I wanted to show multiple approaches to the same problem, as there is no "one set way", at least not to my knowledge.

Let's start by creating ourselves a PhpSpec for this existing form implementation:

php vendor/bin/phpspec desc AppBundle/Form/Type/UploadedFileType

Specification for AppBundle\Form\Type\UploadedFileType created in /path/to/my/wallpaper/spec/AppBundle/Form/Type/UploadedFileTypeSpec.php.

And running this new spec:

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php
                                      100%                                       1
1 specs
1 example (1 passed)
26ms

We need to write some tests for what we already have. Try not to over think this, and let's start with the buildForm method:

<?php

// spec/AppBundle/Form/Type/UploadedFileTypeSpec.php

namespace spec\AppBundle\Form\Type;

use AppBundle\Form\Type\UploadedFileType;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class UploadedFileTypeSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(UploadedFileType::class);
    }

    function it_can_build_a_form(FormBuilderInterface $formBuilder)
    {
        $this->buildForm($formBuilder, []);

        $formBuilder->add('file', FileType::class, [
            'multiple' => false,
        ])->shouldHaveBeenCalled();
    }
}

We've covered how we should not mock what we do not own. However, we don't own FormBuilderInterface, yet we are mocking it anyway.

Well, PhpSpec is generally good to us if what we are trying to mock is an interface, which in this case $formBuilder will be. We've covered mocking interfaces already, and why this is a little confusing.

With a mocked FormBuilderInterface we can call the buildForm method on our form type, passing in our mock and an empty array of options:

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'multiple' => false,
            ])
        ;
    }

Inside our real buildForm implementation we expect add to be called with some given arguments.

As such, after a call to buildForm has been made, we can expect that our mock form builder should have been called with the expected arguments:

        $formBuilder->add('file', FileType::class, [
            'multiple' => false,
        ])->shouldHaveBeenCalled();

Does this pass?

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php
                                      100%                                       2
1 specs
2 examples (2 passed)
56ms

Cool.

Onwards!

Next we need to test the buildView method, and of course, things here won't be quite so simple.

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['full_name'] = $view->vars['full_name'] . '[file]';
    }

FormView is not an interface. This will potentially cause us problems.

Likewise, we aren't using a setter for vars, but instead modifying a public property which happens to contain an array. PhpSpec is almost certainly not going to appreciate this.

Here's the gist of the test:

<?php

// spec/AppBundle/Form/Type/UploadedFileTypeSpec.php

namespace spec\AppBundle\Form\Type;

use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use AppBundle\Form\Type\UploadedFileType;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class UploadedFileTypeSpec extends ObjectBehavior
{
    // * snip *

    function it_can_build_a_view(
        FormView $view,
        FormInterface $form
    )
    {
        $view->vars['full_name'] = 'quidditch';

        $this->buildView($view, $form, []);

        $view->vars['full_name']->shouldReturn('quidditch[file]');
    }
}

Will this work?

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php

AppBundle/Form/Type/UploadedFileType
  30  - it can build a view
      notice: Indirect modification of overloaded property PhpSpec\Wrapper\Collaborator::$vars has no effect in
      /path/to/my/wallpaper/spec/AppBundle/Form/Type/UploadedFileTypeSpec.php line 34

Oh my, no.

We need to heed the advice already espoused: "Don't mock what you don't own."

Let's try again, this time with a real implementation of the FormView:

<?php

// spec/AppBundle/Form/Type/UploadedFileTypeSpec.php

namespace spec\AppBundle\Form\Type;

use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use AppBundle\Form\Type\UploadedFileType;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class UploadedFileTypeSpec extends ObjectBehavior
{
    // * snip *

    function it_can_build_a_view(
        FormInterface $form
    ) {
        $view = new FormView();

        $view->vars['full_name'] = 'quidditch';

        $this->buildView($view, $form, []);

        $view->vars['full_name']->shouldReturn('quidditch[file]');
    }
}

When we run the tests:

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php

AppBundle/Form/Type/UploadedFileType
  30  - it can build a view
      exception [err:Error("Call to a member function shouldReturn() on string")] has been thrown.

Rats, so close!

What's going wrong here?

Well, everything up until the last line of the test seems good.

But here:

$view->vars['full_name']->shouldReturn('quidditch[file]');

We are trying to call a method on a string.

See, $view->vars['full_name'] returns a string.

We cannot call PhpSpec methods on a string.

What to do?

Well, there may be a way to do this properly with PhpSpec, but I am not aware of how to do so.

If this were our own code then we would likely refactor here not use a public property in this way. At least, that would be my first port of call.

However, we don't own Symfony's form implementation, and I'm certainly not wanting to start meddling too deep here just to get PhpSpec to play ball.

Instead - and with a hint of irony - let's use a library from the creator of Symfony's form component to get ourselves out of this problem:

composer require --dev webmozart/assert

With over 22 million installs at the time of writing, we should be in good company here.

Now all we need to do is assert the outcome of $view->vars['full_name'] is as we expect:

<?php

namespace spec\AppBundle\Form\Type;

use AppBundle\Form\Type\UploadedFileType;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Test\FormBuilderInterface;
use Webmozart\Assert\Assert;

class UploadedFileTypeSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(UploadedFileType::class);
    }

    function it_can_build_a_form(FormBuilderInterface $formBuilder)
    {
        $this->buildForm($formBuilder, []);

        $formBuilder->add('file', FileType::class, [
            'multiple' => false,
        ])->shouldHaveBeenCalled();
    }

    function it_can_build_a_view(
        FormInterface $form
    ) {
        $view = new FormView();

        $view->vars['full_name'] = 'quidditch';

        $this->buildView($view, $form, []);

        Assert::same(
            $view->vars['full_name'],
            'quidditch[file]'
        );
    }
}

And now:

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php
                                      100%                                       3
1 specs
3 examples (3 passed)
21ms

Stay tuned for more Happy Days.

The last method to test is configureOptions.

Again, we don't own OptionsResolver and it's not an interface either. Still, in the first instance I always try my luck:

    function it_correctly_configures_the_expected_options(
        OptionsResolver $resolver
    )
    {
        $this->configureOptions($resolver);

        $resolver
            ->setDefaults([
                'data_class' => SymfonyUploadedFile::class
            ])
            ->shouldHaveBeenCalled()
        ;
    }

And whaddayaknow?

php vendor/bin/phpspec run spec/AppBundle/Form/Type/UploadedFileTypeSpec.php
                                      100%                                       4
1 specs
4 examples (4 passed)
66ms

This brings us to a point where we have a working test spec for our existing implementation. Now we're going to change this implementation, but with a confidence of knowing that by the end of this process, if all our tests pass, we didn't break anything.

Code For This Course

Get the code for this course.

Episodes

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:57
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:33
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:01
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:51
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:41
36 Test Driven Wallpaper Delete - Part 1 11:06
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01