Getting Started Testing Wallpaper Updates


In the past few videos we have implemented the 'Update' and 'Delete' functionality required to get us to the state of a working website. However, we have done so without testing anything. Whilst we currently only have two entities in our system, already we have laid ourselves a shaky foundation from which to build upon.

Let's fix that.

To begin with we need to switch back to our tested branch.

To do this we will checkout the code as it was at the end of video 26.

git checkout vid-26 -b tested-wallpaper-edit

This one command checks out the code as it was at the end of vid-26, and both creates and switches us to a new branch named tested-wallpaper-edit.

It would also be a good idea to drop, re-create, migrate, and reload our fixtures.

However, first we will need to address an issue with our fixtures.

As our design is different when working in our tested environment, the fixture changes we made during the 'Fixing the Fixtures' video won't be suitable for this branch.

The change we need to make is not difficult, but there are lots of them. Fortunately I have already done the hard work for you.

The gist of this change is to wrap Symfony's UploadedFile with our own SymfonyUploadedFile class. If you don't understand this process then please watch from this video onwards.

The following is an example of the change needed:

        $file = (new SymfonyUploadedFile())->setFile(
            new UploadedFile(
                $temporaryImagesPath . '/abstract-background-pink.jpg',
                'abstract-background-pink.jpg'
            )
        );
        $wallpaper = (new Wallpaper())
            ->setFile($file)
            ->setFilename('abstract-background-pink.jpg')
            ->setSlug('abstract-background-pink')
            ->setWidth(1920)
            ->setHeight(1080)
            ->setCategory(
                $this->getReference('category.abstract')
            )
        ;
        $manager->persist($wallpaper);

Also, be sure to copy the images folder from web/images to src/AppBundle/DataFixtures/images.

Finally be sure to delete the contents of your web/images directory, but don't delete the directory itself.

Then we should be able to run the following commands:

php bin/console doctrine:database:drop --force
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
php bin/console doctrine:fixtures:load

All of these should run without issue, and give us a known good working state to start from.

Before we get started, it would be a good idea to run through our test suite and evaluate our current position:

php vendor/bin/phpspec run
                                      100%                                       13
5 specs
13 examples (13 passed)
82ms

At this point we need to start re-implementing the concepts and features we came up with during our untested / prototype phase.

To begin with, let's re-create the FileTransformer Data Transformer:

php vendor/bin/phpspec desc AppBundle/Form/DataTransformer/FileTransformer

Specification for AppBundle\Form\DataTransformer\FileTransformer created in /path/to/my/wallpaper/spec/AppBundle/Form/DataTransformer/FileTransformerSpec.php.

➜  wallpaper git:(tested-wallpaper-edit) ✗ php vendor/bin/phpspec run
AppBundle/Form/DataTransformer/FileTransformer
  11  - it is initializable
      class AppBundle\Form\DataTransformer\FileTransformer does not exist.

                                   92%                                      7%   14
6 specs
14 examples (13 passed, 1 broken)
92ms

  Do you want me to create `AppBundle\Form\DataTransformer\FileTransformer`
  for you?
                                                                         [Y/n]
y
Class AppBundle\Form\DataTransformer\FileTransformer created in /path/to/my/wallpaper/src/AppBundle/Form/DataTransformer/FileTransformer.php.

                                      100%                                       14
6 specs
14 examples (14 passed)
40ms

Looking back at our prototype, this class is doing very little, so testing should be quick and easy (and fun!):

<?php

// /src/AppBundle/Form/DataTransformer/FileTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class FileTransformer implements DataTransformerInterface
{
    /**
     * converts the data used in code to a format that can be rendered in the form
     */
    public function transform($file)
    {
        return [
            'file' => null,
        ];
    }

    /**
     * converts the data from the form submission to a format that can be used in code
     */
    public function reverseTransform($data)
    {
        return $data['file'];
    }
}

Looking at this class, we could do better.

The transform function takes a $file argument but doesn't do anything with it.

The reverseTransform function expects to be given an array with a key called file set, but what happens if that key isn't set?

What I've found is that when writing code without tests, I assume the happy path is all there is. In reality, coding is (most of the time for me at least) the process of describing what to do when we are not on the happy path.

Testing helps me think about this ahead of time, which leads to a much more robust implementation. This also makes my life easier in the long run.

Here's the test I'm starting from:

<?php

// spec/AppBundle/Form/DataTransformer/FileTransformerSpec.php

namespace spec\AppBundle\Form\DataTransformer;

use AppBundle\Form\DataTransformer\FileTransformer;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Form\DataTransformerInterface;

class FileTransformerSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(FileTransformer::class);
        $this->shouldImplement(DataTransformerInterface::class);
    }

    function it_can_transform()
    {
        $file = null;

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);

        $file = 'hello';

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

    function it_can_reverse_transform()
    {
        $data = [
            'file' => null
        ];

        $this->reverseTransform($data)->shouldReturn(null);

        $data = [
            'file' => 'my-file'
        ];

        $this->reverseTransform($data)->shouldReturn('my-file');
    }
}

There's a thing I'm doing in these tests that I don't like, but which I haven't found a great solution to.

Take this test:

    function it_can_transform()
    {
        $file = null;

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);

        $file = 'hello';

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

It's the exact same test, twice, but with differing input data.

PhpUnit has a really nice solution to this problem. It's called a Data Provider. It's one of my favourite parts of PhpUnit and one that is unfortunately not available in PhpSpec 3 yet.

What a data provider does is allow you to declare a separate method which returns an array of arrays.

Each array inside the outer array is used as the arguments passed into your test function.

This allows you to define one test, but provide more than one set of inputs. It's very helpful as it also tells you, specifically, which set of inputs caused the test to fail.

Unfortunately, we can't do this in PhpSpec 3 (you can if using PhpSpec 2).

As such we could either define two tests, or run the same test twice - with different inputs - inside one test. Either is a valid option in my viewpoint. Just standardise on one.

    function it_can_transform_1()
    {
        $file = null;

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

    function it_can_transform_2()
    {
        $file = 'hello';

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

Here's the final test spec:

<?php

namespace spec\AppBundle\Form\DataTransformer;

use AppBundle\Form\DataTransformer\FileTransformer;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Form\DataTransformerInterface;

class FileTransformerSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(FileTransformer::class);
        $this->shouldImplement(DataTransformerInterface::class);
    }

    function it_can_transform_1()
    {
        $file = null;

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

    function it_can_transform_2()
    {
        $file = 'hello';

        $this->transform($file)->shouldReturn([
            'file' => $file,
        ]);
    }

    function it_can_reverse_transform_1()
    {
        $data = [
            'file' => null
        ];

        $this->reverseTransform($data)->shouldReturn(null);
    }

    function it_can_reverse_transform_2()
    {
        $data = [
            'file' => 'my-file'
        ];

        $this->reverseTransform($data)->shouldReturn('my-file');
    }
}

I choose to separate out the tests this way as it makes individual failures easier to work with. Try both approaches (or something else entirely) if unsure.

Let's let PhpSpec do the hard work here:

php vendor/bin/phpspec run spec/AppBundle/Form/DataTransformer/FileTransformerSpec.php

AppBundle/Form/DataTransformer/FileTransformer
  12  - it is initializable
      expected an instance of Symfony\Component\Form\DataTransformerInterface, but got
      [obj:AppBundle\Form\DataTransformer\FileTransformer].

AppBundle/Form/DataTransformer/FileTransformer
  18  - it can transform
      method AppBundle\Form\DataTransformer\FileTransformer::transform not found.

AppBundle/Form/DataTransformer/FileTransformer
  33  - it can reverse transform
      method AppBundle\Form\DataTransformer\FileTransformer::reverseTransform not found.

            33%                                     66%                          3
1 specs
3 examples (1 failed, 2 broken)
30ms

  Do you want me to create
  `AppBundle\Form\DataTransformer\FileTransformer::transform()` for you?
                                                                         [Y/n]
y
  Method AppBundle\Form\DataTransformer\FileTransformer::transform() has been created.

  Do you want me to create
  `AppBundle\Form\DataTransformer\FileTransformer::reverseTransform()` for
  you?
                                                                         [Y/n]
y
  Method AppBundle\Form\DataTransformer\FileTransformer::reverseTransform() has been created.

AppBundle/Form/DataTransformer/FileTransformer
  12  - it is initializable
      expected an instance of Symfony\Component\Form\DataTransformerInterface, but got
      [obj:AppBundle\Form\DataTransformer\FileTransformer].

AppBundle/Form/DataTransformer/FileTransformer
  18  - it can transform
      expected [array:1], but got null.

AppBundle/Form/DataTransformer/FileTransformer
  33  - it can reverse transform
      expected "my-file", but got null.

                                      100%                                       3
1 specs
3 examples (3 failed)
11ms

And after running this we have a new file created, along with two method stubs:

<?php

// src/AppBundle/Form/DataTransformer/FileTransformer.php

namespace AppBundle\Form\DataTransformer;

class FileTransformer
{
    public function transform($argument1)
    {
        // TODO: write logic here
    }

    public function reverseTransform($argument1)
    {
        // TODO: write logic here
    }
}

We can be truly lazy here and copy / paste our original implementation over the top:

<?php

// /src/AppBundle/Form/DataTransformer/FileTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class FileTransformer implements DataTransformerInterface
{
    /**
     * converts the data used in code to a format that can be rendered in the form
     */
    public function transform($file)
    {
        return [
            'file' => null,
        ];
    }

    /**
     * converts the data from the form submission to a format that can be used in code
     */
    public function reverseTransform($data)
    {
        return $data['file'];
    }
}

Do our tests pass here?

php vendor/bin/phpspec run spec/AppBundle/Form/DataTransformer/FileTransformerSpec.php

AppBundle/Form/DataTransformer/FileTransformer
  27  - it can transform 2
      expected [array:1], but got [array:1].

                              80%                                     20%        5
1 specs
5 examples (4 passed, 1 failed)
32ms

This is because our transform function ignores the input argument. Let's fix this:

<?php

// /src/AppBundle/Form/DataTransformer/FileTransformer.php

    /**
     * converts the data used in code to a format that can be rendered in the form
     *
     * @param null $file
     * @return array|mixed
     */
    public function transform($file = null)
    {
        return [
            'file' => $file,
        ];
    }

And the tests:

php vendor/bin/phpspec run spec/AppBundle/Form/DataTransformer/FileTransformerSpec.php
                                      100%                                       5
1 specs
5 examples (5 passed)
30ms

This leaves us with a few extra tasks ahead, each one laid out by the prototype we created.

Testing each of these steps is a slower process up front. There's no denying this, nor avoiding it. If you want to test you have to pay the price in full, and up front.

Where this starts to save us time is in the future.

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