An easy way to test 3rd party SDKs with Laravel
Testing external services easily with strong assertions

I am a fullstack engineer and spend most of my time building stuff with NodeJs & PHP. My daily work involves, brainstorming, discovery, running peer reviews, writing feature code, unit tests, etc and helping the team where needed.
Nothing is more satisfying than writing unit tests that test complex functionalities with strong assertions.
Starting with an example
Let's take an example where our app has a mixed functionality which involve external API calls, DB calls and internal business logic.
Our app is using a hypothetical external SDK TwitterSDK. We have a service SocialsService that accepts text for a tweet and does the following:
Sanitise the
textGenerate
tagsfrom text using an external API call.Post
textandtagsusingTwitterSDKon twitter and gettweetIdUse
tweetIdto gettweetAnalyticsIdSave
tweedIdandtweetAnalyticsIdin database using a modelSocials
The bolded points (1 and 5) involve internal business logic and need to be tested correctly. Rest of the points involve 3rd party services, hence we need to test if they are receiving correct arguments.
<?php
use TwitterSDK\TwitterService;
use App\Modules\Social\Models\Social as SocialModel;
namespace App\Modules\Social\Services;
class SocialsService
{
public function __construct(public TwitterService $twitterService) {}
public function tweetAndPersist(string $text): string
{
$text = $this->sanitizeTweetText($text);
$tags = $this->generateTags($text);
$tweetId = $this->twitterService->addTweet($text, $tags);
$analyticsId = $this->twitterService->getAnalyticsId($tweetId);
return $this->persistToDb($tweetId, $analyticsId);
}
public function generateTags(string $text): array
{
$tags = [];
// external HTTP call to generate tags
return [];
}
private function sanitizeTweetText(string $text): string
{
// sanitize text
// ...more business logic
return strip_tags($text);
}
private function persistToDb(string $tweetId, string $analyticsId): string
{
// persist to db
return SocialModel::create(['tweet_id' => $tweetId, 'analytics_id' => $analyticsId]);
}
}
We should be able to test the business logic for these methods correctly
sanitizeTweetText()persistToDb()
We should be able to mock the following methods
generateTags()[Notice how this is a part of our main service class]addTweet()getAnalyticsId()
The classic solution
The classic solution would involve refactoring the code & injecting a mock class that implements the same signature as TwitterService. Also for generateTags() method we'll have to create a new service TagGeneratorService and inject it in the Socials service.
The refactored SocialsService would look like this:
class SocialsService
{
// mock class and original class should implement the interface
public function __construct(
public TwitterServiceInterface $twitterService,
public TagGeneratorServiceInterface $tagGeneratorService,
) {}
// ... rest of the code using these services
}
In the unit test, we'll have to use the service like this:
/** @test */
public function testSocialMediaService(): void
{
$twitterMock = new TwitterServiceMock()
$tagGeneratorMock = new TagGeneratorMock()
$socialsService = new SocialsService($twitterMock, $tagGeneratorMock)
// ... assertions
}
This would involve a lot of refactoring, creating mocks for external SDKs would also take time.
However, what if I don't want to refactor this much and still test the code correctly ?
The easier solution
We will use Mockery to mock expectations for these methods.
The complete test code after mocking would look like this
/** @test */
public function testSocialMediaService(): void
{
$tweetText = "This is a <h1>tweet</h1> about unit tests";
$sanitizedTweet = "This is a tweet about unit tests";
$mockTags = ["#unittests"];
$mockTweetId = 'tweetId';
$mockAnalyticsId = 'analyticsId';
$twitterMock = $this->mock(TwitterService::class, function (MockInterface $mock) use (...) {
$mock->shouldReceive('addTweet')->with($sanitizedTweet, $mockTag)->andReturn($mockTweetId)->once();
$mock->shouldReceive('getAnalyticsId')->with($mockTweetId)->andReturn($mockAnalyticsId)->once();
});
// mock the main service partially
$socialsServiceMock = Mockery::mock(SocialsService::class, [$twitterMock])->makePartial();
$socialsServiceMock->shouldReceive('generateTags')->with($sanitizedTweet)->andReturn($mockTag)->once();
$socialsModel = $socialsServiceMock->tweetAndPersist($tweetText);
$this->assertTrue($tweetId, $socialsModel->tweet_id);
$this->assertTrue($analyticsId, $socialsModel->analytics_id);
}
Let's break it down and understand the different parts
- Mocking the TwitterService with argument assertions
$twitterMock = $this->mock(TwitterService::class, function (MockInterface $mock) use (...) {
$mock->shouldReceive('addTweet')->with($sanitizedTweet, $mockTags)->andReturn($mockTweetId)->once();
$mock->shouldReceive('getAnalyticsId')->with($mockTweetId)->andReturn($mockAnalyticsId)->once();
});
Notice how we are testing that correct arguments are being passed to addTweet(), the argument $sanitizedTweet would actually confirm that our sanitizeTweetText() method is working correctly.
Also notice how we test that $mockTweetId returned by addTweet() is being correctly passed to the getAnalyticsId() method.
- Mocking the
generateTags()method
Now we need to test the generateTags() method, for this we will mock the main service partially i.e methods which are not mocked will run normally.
// mock the main service partially, pass $twitterMock in constructor
$mock = Mockery::mock(SocialsService::class, [$twitterMock])->makePartial();
$mock->shouldReceive('generateTags')->with($sanitizedTweet)->andReturn($mockTags)->once();
// methods other than generateTags() run normally
Notice how we test that the sanizited text $sanitizedTweet is being passed to the generateTags() method, we obviously return a mock tag as we don't want to run the method.
Conclusion
We have correctly mocked the external SDK calls without losing assertions for business logic.




