Defensive Counter Measures


Towards the end of the previous video we had started writing the tests / specification of how our WallpaperUploadListener service should work.

We had seen how we would need to react to creating new, and updating existing Wallpaper entities by way of Doctrine's prePersist and preUpdate events.

As part of this process we would need to move the uploaded wallpaper file from wherever PHP is temporarily storing the uploaded file, to a location on disk that we control. To do this we could use our FileMover service.

In order to access the FileMover service we would need to inject it into our WallpaperUploadListener.

From personal experience, this is where testing starts to become a little more confusing.

We had a service we control that relies on a service we control. How on Earth do we test this? Let's crack on, and find out.

Testing Nested Dependencies

The tricky part of our test setup now is that we want to ensure that FileMover::move has been been called with some specific arguments, but we don't directly have access to those arguments any more.

Let's take a moment to think about where these two arguments may come from. As a quick recap, here's our test spec so far:

// /spec/AppBundle/Event/Listener/WallpaperUploadListenerSpec.php

    function it_can_prePersist(LifecycleEventArgs $eventArgs)
    {
        $fakeTempPath = '/tmp/some.file';
        $fakeRealPath = '/path/to/my/project/some.file';

        $this->prePersist($eventArgs);

        $this->fileMover->move($fakeTempPath, $fakeRealPath)->shouldHaveBeenCalled();
    }

This is a failing test at present.

It fails because we're making an assumption:

We expect that after prePersist has been called with the given $eventArgs, that the fileMover->move method should have been called as part of this process, using the $fakeTempPath and $fakeRealPath variables.

The problem here is that $fakeTempPath and $fakeRealPath are not being passed to the prePersist call, so how would it have any knowledge about them?

It doesn't... so the test fails.

php vendor/bin/phpspec run spec/AppBundle/Event/Listener/WallpaperUploadListenerSpec.php

AppBundle/Event/Listener/WallpaperUploadListener
  28  - it can prePersist
      no calls have been made that match:
        Double\AppBundle\Service\FileMover\P2->move(exact("/tmp/some.file"), exact("/path/to/my/project/some.file"))
      but expected at least one.

                         66%                                     33%             3
1 specs
3 examples (2 passed, 1 failed)
67ms

At this stage we have no implementation logic at all in our real prePersist method.

We know that as part of this process we expect, at some point, to call the fileMover->move method.

This move method expects these two arguments.

Behind the scenes - and outside our control - when our prePersist method is called, it will be called with an instance of LifecycleEventArgs. We don't need to worry about how this happens, we are simply hooking into this process to help ourselves.

If we get given this LifecycleEventArgs object, we can make use of the public methods it provides. These are:

  • getEntity
  • getEntityManager

There are a bunch of ways to figure out what this object's public methods are. To figure this out, a quick trip to the documentation will tell us all we need to know.

Getting access to the entity manager may be helpful to us a little later on.

More important to us immediately is being able to access the entity. There is a little gotcha here that we need to take care of - and it's certainly not immediately obvious either.

If you remember, a few videos back we discussed how if we hook into Doctrine's lifecycle events, our WallpaperUploadListener's prePersist and preUpdate event listener methods will be called every time an entity is either persisted, or updated.

This means this code will be called for every entity. Not just wallpapers, but Category, and any other entities we create in our project.

Our WallpaperUploadListener really needn't concern itself with anything other than Wallpaper objects.

Let's get defensive and ensure we only run this code if we are working with a Wallpaper instance:

// /spec/AppBundle/Event/Listener/WallpaperUploadListenerSpec.php

    function it_returns_early_if_prePersist_LifecycleEventArgs_entity_is_not_a_Wallpaper_instance(
        LifecycleEventArgs $eventArgs
    )
    {
        $this->prePersist($eventArgs)->shouldReturn(false);
    }

This test isn't really helping us that much just yet.

It's also pretty confusing, and the name is not helping.

But don't be put off, we are actively working on this code and are still in the exploratory phase. Things may look and feel a little rough around the edges. We will smooth them out as we go.

This test still fails as our implementation doesn't do anything - if we ran the test now it would fail as returning null, rather than the expected false.

We could do the bare minimum to make this test pass:

// /src/AppBundle/Event/Listener/WallpaperUploadListener.php

    public function prePersist(LifecycleEventArgs $eventArgs)
    {
        return false;
    }

And whilst we now have a passing test, again it's fairly meaningless.

What we want to check is that if the entity returned by a called to $eventArgs->getEntity(); is anything other than a Wallpaper, then we expected to immediately return false;.

Otherwise, we will continue on and do "other stuff".

Let's add this logic to our prePersist method:

// /src/AppBundle/Event/Listener/WallpaperUploadListener.php

    public function prePersist(LifecycleEventArgs $eventArgs)
    {
        if (false === $eventArgs->getEntity() instanceof Wallpaper) {
            return false;
        }

        return true;
    }

Great, now our test should pass and there is a little more sense as to why.

But we aren't done here just yet.

It might be that we have a few places inside this method where we return false. Maybe we return false if any of our guard clauses fail. Maybe we have a few guard clauses in our method.

Consider The Unhappy Path

It would be nice to ensure other things did not happen here. One such thing is checking that our file mover did not try to move any files.

We can add this into our test logic:

// /spec/AppBundle/Event/Listener/WallpaperUploadListenerSpec.php

use Prophecy\Argument; // make sure to add in this use statement

    function it_returns_early_if_prePersist_LifecycleEventArgs_entity_is_not_a_Wallpaper_instance(
        LifecycleEventArgs $eventArgs
    )
    {
        $this->prePersist($eventArgs)->shouldReturn(false);

        $this->fileMover->move(
            Argument::any(),
            Argument::any()
        )->shouldNotHaveBeenCalled();
    }

Ok, things just got a little stranger :)

Let's cover this in a little more depth.

We have our original assertion - that if we try to call prePersist, and the returned value from $eventArgs->getEntity(); is not an instance of Wallpaper, then we should return false;.

None of that has changed.

You may be curious as to exactly what is being returned when PHP actually runs $eventArgs->getEntity(); - to answer this, it will be null. If we don't explicitly tell our PhpSpec setup what to return for a given method call, it will return null by default.

This test passes because null is not an instance of Wallpaper :)

We can make this more explicit. We can tell our $eventArgs->getEntity(); call that, when called, it should return something else:

// /spec/AppBundle/Event/Listener/WallpaperUploadListenerSpec.php

use AppBundle\Entity\Category; // make sure to add in this use statement

    function it_returns_early_if_prePersist_LifecycleEventArgs_entity_is_not_a_Wallpaper_instance(
        LifecycleEventArgs $eventArgs
    )
    {
        $eventArgs->getEntity()->willReturn(new Category());

        $this->prePersist($eventArgs)->shouldReturn(false);
    }

Here we are being more explicit. We are saying that when the getEntity method is called on the collaborator $eventArgs object, we want to pretend a new Category instance was returned.

Again, a Category is not an instanceof Wallpaper, so this guard clause is met, and our test still passes.

Essentially we are able to set up our system in such a way that we can test how it responds to very specific situations and circumstances. So long as we provide all the known circumstances that should happen in production, we can ensure our code behaves as expected. Quite cool :)

But let's get back to this line:

use Prophecy\Argument;

        $this->fileMover->move(
            Argument::any(),
            Argument::any()
        )->shouldNotHaveBeenCalled();

We checked that our method return false;.

It would be super useful to double check that somehow the move method was not also inadvertently called.

We know that when we call move, we must pass in two arguments - the path to move from, and the path to move to.

We're making an assertion here. We're describing in our test code a task that we're interested in observing.

We haven't yet covered where these two bits of data will come from. We don't need to worry about the specifics though, we can use the underlying Prophecy library (included as part of PhpSpec) to help us preemptively describe our way through this process.

Argument::any() is going to allow us to check that this function was called with any argument - string, int, SomeSpecialType - or in our case, was not called with any arguments.

I like to think of this as my insurance policy. It doesn't matter what arguments this function is being called with at this point, as we don't want it to be called at all.

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