Courses

Testing in Laravel 11: Advanced Level

Testing Third-Parties by Mocking Classes

And now, we finally get to the Mocking part. This is probably the most requested topic before this Advanced course.

I deliberately postponed the mocking topic after all the faking so that you would understand that mocking is the same topic as faking classes or other behaviors.


Example Scenario

Suppose we have a Product model with youtube_id and youtube_thumbnail columns, where values are taken from the external Youtube API.

To get that thumbnail, you create a separate Service class called YoutubeService with the method getThumbnailByID().

Then, in the Controller, when creating the record, if the youtube_id field is set, youtube_thumbnail will be auto-set from YoutubeService.

class ProductController extends Controller
{
public function store(StoreProductRequest $request, YouTubeService $youTubeService)
{
$productData = $request->validated();
 
if ($request->youtube_id) {
$prooductData['youtube_thumbnail'] = $youTubeService->getThumbnailByID($request->youtube_id);
}
 
$product = Product::create($productData);
 
return redirect()->route('products.index');
}
}

Now, what will that YoutubeService look like:

app/Services/YoutubeService.php:

class YouTubeService
{
public function getThumbnailByID(string $youtubeID): string
{
$response = Http::asJson()
->baseUrl('https://youtube.googleapis.com/youtube/v3/')
->get('videos', [
'part' => 'snippet',
'id' => $youtubeID,
'key' => config('services.youtube.key'),
])->collect('items');
 
return $response[0]['snippet']['thumbnails']['default']['url'];
}
}

This service may be more complex, but the main thing is that it makes the API request to collect the thumbnail. The tricky part is that we need to provide the API key: YouTube is not a public API and won't give us the data without the key.

We can test this behavior in PHPUnit from two angles:

  • First, test the actual service class so that it works correctly
  • Or, you may test the actual HTTP request of your project and skip the YouTube service by faking its data.

In most cases, if you use the API correctly, following the official docs, you can be pretty sure that it will return the correct result. There's quite a little chance that the YouTube API will go down, right?


The Test

So, in your tests, you better test your application behavior, assuming the external API call will succeed. Let's see an example of Mocking.

$this->mock(YouTubeService::class)
->shouldReceive('getThumbnailByID')
->with('5XywKLjCD3g')
->once()
->andReturn('https://i.ytimg.com/vi/5XywKLjCD3g/default.jpg');

Here, we mock (fake) the YouTubeService class and its getThumbnailByID() method, to which we pass the parameter. Then, we tell it to run only once for that request and return the desired result.

It might seem complex at first glance, but what it means is it will NOT actually execute the method, replacing the call to that method with the result we hardcode. In our example, everything inside getThumbnailByID() will be ignored.

In other words, we are not testing the YouTube API itself. We are testing that our application Service class method will be executed during the request lifecycle.

The whole test could look like this:

use function Pest\Laravel\actingAs;
 
beforeEach(function () {
$this->user = User::factory()->create();
});
 
test('store product exists in database', function () {
$this->mock(YouTubeService::class)
->shouldReceive('getThumbnailByID')
->with('5XywKLjCD3g')
->once()
->andReturn('https://i.ytimg.com/vi/5XywKLjCD3g/default.jpg');
 
actingAs($this->user)
->post('/products', [
'name' => 'Product 123',
'price' => 1234,
]);
 
expect(Product::latest()->first())
->name->toBe('Product 123')
->and->price->toBe(1234);
});

Notice we're not calling the getThumbnailByID() directly. We're making a POST request to the URL that points to the Controller that would call that Service method.

The critical part here is the code's structure. For example, in the Controller, if you call the YouTube API directly instead of a service, you won't be able to mock it because it is not a separate unit. So, to utilize mocking, you need to separate the parts into their own units, like with Service methods.

No comments or questions yet...