Using PhpSpec to Test our FileMover


Now we need to consider how this move process actually happens.

We said earlier that out of the three immediately obvious options, we are going to use the Symfony Filesystem component.

This being a Symfony service, we know by now that if we want to use this component we need to do a few things.

We need to inject an implementation of the Filesystem into this service.

This means we will need to create or update our service definition for our FileMover. Well, we don't yet have a service, so let's create one. And whilst creating one, let's inject the Filesystem:

# /app/config/services.yml

services:

    app.wallpaper_mover:
        class: AppBundle\Service\FileMover

That's the basis of our service definition.

Next, we want to pass in the Filesystem. How do we do this? We need to pass in the service id that represents the Filesystem inside Symfony:

php bin/console debug:container filesystem

Information for Service "filesystem"
====================================

 ------------------ -----------------------------------------
  Option             Value
 ------------------ -----------------------------------------
  Service ID         filesystem
  Class              Symfony\Component\Filesystem\Filesystem
  Tags               -
  Public             yes
  Synthetic          no
  Lazy               no
  Shared             yes
  Abstract           no
  Autowired          no
  Autowiring Types   -
 ------------------ -----------------------------------------

The Service ID option is what we need. Our service will therefore be @filesystem. Don't forget the @ symbol prefix.

# /app/config/services.yml

services:

    app.wallpaper_mover:
        class: AppBundle\Service\FileMover
        arguments:
            - "@filesystem"

And inside our implementation once more:

<?php

// /AppBundle/Service/FileMover.php

namespace AppBundle\Service;

use Symfony\Component\Filesystem\Filesystem;

class FileMover
{
    private $fileSystem;

    public function __construct(Filesystem $fileSystem)
    {
        $this->fileSystem = $fileSystem;
    }

    public function move($existingFilePath, $newFilePath)
    {
        // TODO: write logic here
    }
}

We only need to look in the Symfony docs for the Filesystem to figure out the next step.

    public function move($existingFilePath, $newFilePath)
    {
        $this->fileSystem->rename($existingFilePath, $newFilePath);
    }
}

If we run our test now though, we've broken both tests:

php vendor/bin/phpspec run

AppBundle/Service/FileMover
  23  - it is initializable
      exception [err:ArgumentCountError("Too few arguments to function AppBundle\Service\FileMover::__construct(), 0 passed and exactly 1 expected")] has been thrown.

AppBundle/Service/FileMover
  28  - it can move a file from temporary to controlled storage
      exception [err:ArgumentCountError("Too few arguments to function AppBundle\Service\FileMover::__construct(), 0 passed and exactly 1 expected")] has been thrown.

                                      100%                                       2
1 specs
2 examples (2 broken)
27ms

The issue is that we're now reliant on the Filesystem when constructing our FileMover. PhpSpec, however, is not aware of any of this.

What we need to do is to tell PhpSpec that when constructing our FileMover instance, it should be given an instance of Filesystem.

Unlike in our services, we won't use the constructor in a PhpSpec to do this. Instead, we could inject an object that resembles a Filesystem into each function:

<?php

// /spec/AppBundle/Service/FileMover.php

namespace spec\AppBundle\Service;

use AppBundle\Service\FileMover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Filesystem\Filesystem;

class FileMoverSpec extends ObjectBehavior
{
    function it_is_initializable(Filesystem $filesystem)
    {
        $this->beConstructedWith($filesystem);

        $this->shouldHaveType(FileMover::class);
    }

    function it_can_move_a_file_from_temporary_to_controlled_storage(Filesystem $filesystem)
    {
        $this->beConstructedWith($filesystem);

        $temporaryPath = '/some/fake/temporary/path';
        $controlledPath = '/some/fake/real/path.ext';

        $this->move($temporaryPath, $controlledPath)->shouldReturn(true);

        $this->filesystem->rename($temporaryPath, $controlledPath)->shouldHaveBeenCalled();
    }
}

However, this is a little repetative (and tedious) if you have a bunch of examples in your spec. Instead, it is better to move this common construction logic in to the let function:

<?php

// /spec/AppBundle/Service/FileMover.php

namespace spec\AppBundle\Service;

use AppBundle\Service\FileMover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Filesystem\Filesystem;

class FileMoverSpec extends ObjectBehavior
{
    private $filesystem;

    function let(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;

        $this
            ->beConstructedWith($filesystem)
        ;
    }

    function it_is_initializable()
    {
        $this->shouldHaveType(FileMover::class);
    }

    function it_can_move_a_file_from_temporary_to_controlled_storage()
    {
        $temporaryPath = '/some/fake/temporary/path';
        $controlledPath = '/some/fake/real/path.ext';

        $this->move($temporaryPath, $controlledPath)->shouldReturn(true);
    }
}

Same outcome, but a little tidier.

Now, if we were to run our tests - surely they pass now, right?

Wrong:

php vendor/bin/phpspec run

AppBundle/Service/FileMover
  33  - it can move a file from temporary to controlled storage
      expected true, but got null.

                  50%                                     50%                    2
1 specs
2 examples (1 passed, 1 failed)
40ms

Ok, back to one passing and our new test is still failing.

The error tells us why:

expected true, but got null.

This becomes obvious if we look at the move implementation:

    public function move($existingFilePath, $newFilePath)
    {
        $this->fileSystem->rename($existingFilePath, $newFilePath);
    }
}

We don't return anything. This method is void, but we expect it to return true.

It turns out that the rename method in Symfony's Filesystem is also void.

It will throw if things go wrong, but it won't return anything on success.

We will simply return true then ourselves. If the rename / move process fails for any reason, this will throw and we'll deal with it elsewhere as appropriate.

    public function move($existingFilePath, $newFilePath)
    {
        $this->fileSystem->rename($existingFilePath, $newFilePath);

        return true;
    }
}

Ok, test time:

php vendor/bin/phpspec run
                                      100%                                       2
1 specs
2 examples (2 passed)
69ms

Cool, it passes.

But what have we tested here?

We have tested that the class exists, is constructable, and that it returns true when calling move.

We've gone to all this trouble of injecting the Filesystem but we haven't checked the expected method has been called. Let's fix that:

<?php

// /spec/AppBundle/Service/FileMover.php

namespace spec\AppBundle\Service;

use AppBundle\Service\FileMover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Filesystem\Filesystem;

class FileMoverSpec extends ObjectBehavior
{
    private $filesystem;

    function let(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;

        $this
            ->beConstructedWith($filesystem)
        ;
    }

    function it_is_initializable()
    {
        $this->shouldHaveType(FileMover::class);
    }

    function it_can_move_a_file_from_temporary_to_controlled_storage()
    {
        $temporaryPath = '/some/fake/temporary/path';
        $controlledPath = '/some/fake/real/path.ext';

        $this->move($temporaryPath, $controlledPath)->shouldReturn(true);

        // new line here
        $this->filesystem->rename($temporaryPath, $controlledPath)->shouldHaveBeenCalled();
    }
}

Now we can spy on the rename method and ensure that not only was it called, but that it was called with the expected properties. This gives us much more confidence.

Ok, we've smashed through a whole host of PhpSpec here, and we've got the foundations of our file upload process in place. We're going to continue on with this in the next video, building up a robust file upload and move process that we can rely on as our site grows.

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