Handcrafting mocks

This empty constructor will allow this class to be created without having to pass the TwitterOAuth that's required for the base class.namespace TestsMocks;use PHPUnitFrameworkTestCase;class Twitter extends AppServicesTwitterTwitter{ /** @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 TestsModels;use AppModelsBlogPost;use TestsTestCase;use AppServicesTwitterTwitter;use TestsMocksTwitter 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 AppServicesTwitterTwitter;use IlluminateFoundationTestingTestCase as BaseTestCase;use TestsMocksTwitter 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 AppServicesTwitterTwitter;use IlluminateFoundationTestingTestCase as BaseTestCase;use TestsMocksTwitter as TwitterMock;use IlluminateFoundationTestingConcernsInteractsWithContainer;abstract class TestCase extends BaseTestCase{ use InteractsWithContainer; // ….protected function fakeTwitter(): TwitterMock { $this->swap(Twitter::class, new TwitterMock()); return app(Twitter::class); }}With that fakeTwitter method in place, your test can be refactored to this:namespace TestsModels;use AppModelsBlogPost;use TestsTestCase;use AppServicesTwitterTwitter;use TestsMocksTwitter as TwitterMock;class BlogPostTest extends TestCase{ /** @var TestsMocksTwitter */ 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 AppServicesTwitterContracts;interface Twitter { public function tweet(string $status)}You should let the actual Twitter class implement the interface.namespace AppServicesTwitter;use AppServicesTwitterContractsTwitter as TwitterInterfaceclass 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 TestsMocks;use AppServicesTwitterContractsTwitter as TwitterInterfaceclass Twitter implements TwitterInterface{ // …}In the service provider you should use that interface to bind the concrete class.namespace AppServicesTwitter;use IlluminateSupportServiceProvider;use AppServicesTwitterContractsTwitter as TwitterInterfaceclass 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 AppServicesTwitterTwitter;use IlluminateFoundationTestingTestCase as BaseTestCase;use TestsMocksTwitter as TwitterMock;use AppServicesTwitterContractsTwitterabstract class TestCase extends BaseTestCase{ // ….protected function fakeTwitter(): TwitterMock { $this->swap(Twitter::class, new TwitterMock()); return app(Twitter::class); }}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!. More details

Leave a Reply