Datadog collects and monitors your PHP app metrics and distributed traces in real-time with application performance monitoring. Decrease downtime and performance issues with Datadog APM by tracing requests across service boundaries and drilling into individual traces end-to-end with flame graphs. Start your 14-day trial for free today.

Handcrafting mocks

Original – by Freek Van der Herten – 7 minute read

In an application I was working on I wanted to implement automated tweets. Of course, this logic should also be tested. In this blogpost I'd like to show you how you can easily handcraft your own mocks.

Setting things up

Let's first take a look at how you can tweet something in PHP. We're going to use Laravel in this example. In that framework, it's common to set things up in a service provider. To authenticate we're going to use the popular abraham/twitteroauth package. In the service provider below we're making sure that whenever we resolve an instance of App\Services\Twitter (we're going to take a look at that soon). It gets a configured TwitterOAuth injected via its constructor.

namespace App\Services\Twitter;

use Abraham\TwitterOAuth\TwitterOAuth;
use Illuminate\Support\ServiceProvider;

class TwitterServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(Twitter::class, function () {
            $connection = new TwitterOAuth(
                config('services.twitter.consumer_key'),
                config('services.twitter.consumer_secret'),
                config('services.twitter.access_token'),
                config('services.twitter.access_token_secret')
            );

            return new Twitter($connection);
        });
    }
}

Let's look at the actual Twitter class. We just have to call the statuses/update endpoint to actually tweet something out.

namespace App\Services\Twitter;

use Abraham\TwitterOAuth\TwitterOAuth;

class Twitter
{
    /** @var \Abraham\TwitterOAuth\TwitterOAuth  */
    protected $twitter;

    public function __construct(TwitterOAuth $twitter)
    {
        $this->twitter = $twitter;
    }

    public function tweet(string $status)
    {
        return $this->twitter->post('statuses/update', ['status' => $status]);
    }
}

Imagine you want to tweet something out as soon as a blog post is published. There are multiple ways to code up that behaviour. In the example below if chosen to just fire off an event when the blog post has been published.

use Illuminate\Database\Eloquent\Model;

class BlogPost extends Model {
  
  // ...

   public function publish()
   {
	  $this->published_at = now();
	  $this->save();

	  event(new BlogPostPublished($this));

	  return $this;
   }
  
   public function tweetText(): string
   {
       return "Here's my new blogpost: {$this->url}";
   }
}

In our app we’ve set up an event listener that will actually send the tweet when the BlogPostPublished event is fired. I'm not going to include that code in this blog post as it's not relevant to the test we're going to perform.

How to test that the tweet is sent?

In our test, we want to make sure that when publishing something the tweet is actually sent. Of course, we don't want actually tweet stuff when running our tests. This would make our tests slow and our followers nuts.

namespace Tests\Models;

use App\Models\Event;
use Tests\TestCase;

class BlogPostTest extends TestCase
{
    /** @test */
    public function it_will_send_a_tweet_when_publishing_a_post()
	{
        $blogPost = factory(BlogPost::class)->create();
        
        $blogPost->publish();
        
        // how to assert the tweet was sent?
    }
}

Now your first thought could be to reach PHPUnit's built-in mocking solution or Mockery. While these options are perfectly valid, I don't like the fact that adding specific asserts isn't that readable in most cases.

You could also use facades to mock Twitter. This technique is demonstrated Adam Wathan's Test Driven Laravel course. The idea is that you swap out the actual Twitter class with another implementation you have control. This swapping is done via a facade. While this is also a valid approach you need to do some setup to make it work.

Handcrafting mocks

For this small app, I wanted to have the testing as light as possible. It turns out that creating a mock of your own is very simple. You can extend the original class and replace methods with your own implementation.

The trick to making this work is to add an empty constructor in there. This empty constructor will allow this class to be created without having to pass the TwitterOAuth that's required for the base class.

namespace Tests\Mocks;

use PHPUnit\Framework\TestCase;

class Twitter extends \App\Services\Twitter\Twitter
{
    /** @var array */
    protected $sentTweets = [];

    /*
     * This avoids having to pass the constructor parameters defined in the base class
     */
    public function __construct()
    {
    }
    
    public function tweet(string $status)
    {
        $this->sentTweets[] = $status;
    }

    public function assertTweetSent(string $status)
    {
        TestCase::assertContains($status, $this->sentTweets, "Tweet `{$status}` was not sent");
    }
}

With this handcrafted mock, testing that the tweet is sent becomes very easy.

namespace Tests\Models;

use App\Models\BlogPost;
use Tests\TestCase;
use App\Services\Twitter\Twitter;
use Tests\Mocks\Twitter as TwitterMock;

class BlogPostTest extends TestCase
{
    /** @test */
    public function it_will_send_a_tweet_when_publishing_a_post()
    {
        $twitter = $this->app->bind(Twitter::class, function () {
            return new TwitterMock();
        });

        $blogPost = factory(BlogPost::class)->create();

        $blogPost->publish();

        $twitter->assertTweetSent($blogPost->tweetText());
    }
}

If you want to mock Twitter in other tests as well you could move that binding logic to a fakeTwitter method on the base TestCase.

namespace Tests;

use App\Services\Twitter\Twitter;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Mocks\Twitter as TwitterMock;

abstract class TestCase extends BaseTestCase
{
    // …

    protected function fakeTwitter(): TwitterMock
    {
        $twitter = $this->app->bind(Twitter::class, function() {
           return new TwitterMock();
        });   
                
        return $twitter;
    }
}

You can clean that up even more by using Laravel's native InteractsWithContainer trait. It contains a method to easily swap an implementation in the container.

namespace Tests;

use App\Services\Twitter\Twitter;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Mocks\Twitter as TwitterMock;
use Illuminate\Foundation\Testing\Concerns\InteractsWithContainer;

abstract class TestCase extends BaseTestCase
{
    use InteractsWithContainer;

    // ...

    protected function fakeTwitter(): TwitterMock
    {
        return $this->swap(Twitter::class, new TwitterMock());
    }
}

With that fakeTwitter method in place, your test can be refactored to this:

namespace Tests\Models;

use App\Models\BlogPost;
use Tests\TestCase;
use App\Services\Twitter\Twitter;
use Tests\Mocks\Twitter as TwitterMock;

class BlogPostTest extends TestCase
{
    /** @var \Tests\Mocks\Twitter */
    protected $twitter;

    public function setUp()
    {
        parent::setUp();

        $this->twitter = $this->fakeTwitter();
    }

    /** @test */
    public function it_will_send_a_tweet_when_publishing_a_post()
    {
        $blogPost = factory(BlogPost::class)->create();

        $blogPost->publish();

        $this->twitter->assertTweetSent($blogPost->tweetText);
    }

    // moarrr tests
}

If you need additional assertions in other tests it's easy to add them to your mock.

public function assertTweetsSentCount(int $count)
{
    TestCase::assertCount($count, $this->tweets);
}

Using an interface

In the example above the Twitter class is mocked in the most easy way: by extending the class. If you prefer, you could also use an interface.

First, create an interace containing that tweet method.

namespace App\Services\Twitter\Contracts;

interface Twitter 
{
    public function tweet(string $status)
}

You should let the actual Twitter class implement the interface.

namespace App\Services\Twitter;

use App\Services\Twitter\Contracts\Twitter as TwitterInterface;

class Twitter implements TwitterInterface
{
   // ...
}

Your mock shouldn't extend the Twitter class anymore but implement the interface. You don't need to add that empty constructor anymore.

namespace Tests\Mocks;

use App\Services\Twitter\Contracts\Twitter as TwitterInterface

class Twitter implements TwitterInterface
{
   // ...
}

In the service provider you should use that interface to bind the concrete class.

namespace App\Services\Twitter;

use Illuminate\Support\ServiceProvider;
use App\Services\Twitter\Contracts\Twitter as TwitterInterface;

class TwitterServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(TwitterInterface::class, function () {
            $connection = // ...
            return new Twitter($connection);
        });
    }
}

And of course you should also use that interface in the fakeTwitter function in your tests.

namespace Tests;

use App\Services\Twitter\Twitter;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Tests\Mocks\Twitter as TwitterMock;
use App\Services\Twitter\Contracts\Twitter

abstract class TestCase extends BaseTestCase
{
    // ...

    protected function fakeTwitter(): TwitterMock
    {
        return $this->swap(Twitter::class, new TwitterMock());
    }
}

The big benefit of using an interface is that the actual Twitter class and the mocked class are guaranteed to have the right methods.

If you should use this is up to you. Do you like the simplicity of just extending a class? Do that, but be aware that you should keep your class and mock in sync manually.

In conclusion

Of course this is just one of the many possibilities to test this kind of code. Want to use an interface this, perfect. Want to use a regular mock, that’s good too (benefit: you don’t have to maintain your custom mock class).

What are your thoughts on this? Let me know in the comments below!

A big thank you to Frederick Vanbrabant and Brent Roose for reviewing this post.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

Webmentions

Jelenik liked on 3rd July 2020
Joe Campo liked on 3rd July 2020
Macsi liked on 3rd July 2020
Spatie retweeted on 3rd July 2020
Mart Dingley liked on 3rd July 2020
Ken V. liked on 3rd July 2020
Marc Hampson replied on 3rd July 2020
A well timed post. Thanks
Marc Hampson liked on 3rd July 2020
Tinus replied on 3rd July 2020
Can factory be injected? avoiding external unknown dependency
Zach Williams liked on 3rd July 2020
Mariano Moreyra liked on 3rd July 2020
Franck Mercado liked on 3rd July 2020
Michaël De Boey liked on 3rd July 2020
Stephen Shead liked on 3rd July 2020
Francisco Neves liked on 3rd July 2020
Milan Felix Šulc liked on 2nd July 2020
Joey liked on 2nd July 2020
Andy Hartnett retweeted on 2nd July 2020
Vitor Hugo R liked on 2nd July 2020
Salman Zafar liked on 2nd July 2020
Zaiman Noris liked on 2nd July 2020
George Drakakis 🖖 liked on 2nd July 2020
tefo liked on 2nd July 2020
Joao Pinell liked on 2nd July 2020
Roberto B 🚀 liked on 2nd July 2020
Luigi Cruz liked on 2nd July 2020
Johan Alvarez liked on 2nd July 2020
mrdth liked on 2nd July 2020
AshishDhamala liked on 2nd July 2020
Denis Priebe liked on 2nd July 2020
Simon Kollross liked on 2nd July 2020
Bill Yanelli replied on 2nd July 2020
This is how I prefer to mock things! Feeling vindicated rn 😌
Luis van den Bussche liked on 2nd July 2020
Lucas Vasconcelos liked on 2nd July 2020
François liked on 2nd July 2020
Bill Yanelli liked on 2nd July 2020
Vaggelis Yfantis liked on 2nd July 2020
Luka Peharda liked on 2nd July 2020
이현석 Hyunseok Lee liked on 2nd July 2020
Daniel liked on 2nd July 2020
Fabio retweeted on 2nd July 2020
Niels liked on 2nd July 2020
Lawrence Enehizena liked on 2nd July 2020
Fabio liked on 2nd July 2020
Wade Striebel liked on 2nd July 2020
Jigal Sanders liked on 2nd July 2020
Anthony liked on 2nd July 2020
Trilochan Parida liked on 2nd July 2020
Cao Minh Đức retweeted on 2nd July 2020
Dmitry Bubyakin liked on 2nd July 2020
James Kingsley replied on 2nd July 2020
If you're mocking an integral part of the process, is it still useful testing?
Cao Minh Đức liked on 2nd July 2020
/dev/faisal liked on 2nd July 2020
kyawminlwin liked on 2nd July 2020
Geordie Jackson liked on 2nd July 2020
Matej Drame liked on 2nd July 2020
NoMad42 liked on 2nd July 2020
Martin Carlin liked on 2nd July 2020
Andre Sayej liked on 2nd July 2020
Freddy Vargas liked on 2nd July 2020
Arash liked on 2nd July 2020
Rocco Howard liked on 2nd July 2020
Aaron Heath liked on 2nd July 2020
The Beyonder 🔥 liked on 2nd July 2020
Adrian Nürnberger 🐙 replied on 2nd July 2020
Would swap also work?
Freek Van der Herten replied on 2nd July 2020
Yeah think so!
UT liked on 2nd July 2020