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.
Top comments (0)