Invoker Logo

Invoker — The no-bull Laravel tool

Go back to Blog

Testing API calls in your Laravel package

Diana Scharf

In this short tutorial we'll have a look on how to test a Laravel package that uses an external API to retrieve data.

Let's say we implemented a class DogFacts that fetches random dog facts from the DogFactsAPI. We want to test its static method getRandomFacts(int $number) which returns a specified number of n random dog facts as Illuminate\Support\Collection.

Imagine the DogFacts class like this:

<?php

namespace BeyondCode\DogFacts;

use Illuminate\Support\Collection;

class DogFacts {

    public static function getRandomFacts(int $number): Collection {
        // Retrieving and processing data from the API.
        // The API handling could be done by using Laravel's `Http` Facade 
        // for example, but this doesn't matter for this tutorial.
    }
}

We are using orchestral/testbench for testing our Laravel package. The Testbench boots a Laravel application during runtime and therefore provides all kinds of handy helpers.

Let's write a short test. We want to receive 5 random dog facts and check this behaviour by asserting that the getRandomFacts functions return value is Iterable and has a count of 5.

<?php

namespace BeyondCode\DogFacts\Tests;

use Orchestra\Testbench\TestCase;
use BeyondCode\DogFacts\DogFacts;

class DogFactsTest extends TestCase {

    /** @test */
    public function it_returns_a_collection_with_random_facts() {
  
        // Watch out: A real API call is happening here!
        $facts = DogFacts::getRandomFacts(5);

        $this->assertIsIterable($facts);
        $this->assertCount(5, $facts);
    }
}  

In this simple case where we don't need any Authentication for the API, we could run the test just like that and it would work.

But we would make a real API call whenever this test runs! This can be problematic when your tests run in some kind of CI and in addition to that it stresses the API which the could limit the amount of requests and doesn't answer anymore. Or, the most trivial case: It's not available for a short time for some reason.

The good thing is, we are working inside a Laravel environment, so we can make use of the great Http::fake() method.

The fake() method of the Http facade acts with the built-in HTTP client and tells it to return dummy responses when a call to a specific endpoint is made.

Let's try it out!

<?php

namespace BeyondCode\DogFacts\Tests;

use Orchestra\Testbench\TestCase;
use BeyondCode\DogFacts\DogFacts;

class DogFactsTest extends TestCase {

    /** @test */
    public function it_returns_a_collection_with_random_facts() {
  
        Http::fake([
          'https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?number=5' => 
          Http::response(["foo" => "bar"], 200)
        ]);

        $facts = DogFacts::getRandomFacts(5);

        $this->assertIsIterable($facts);
        $this->assertCount(5, $facts);
    }
}

The fake() function expects an array as parameter. You can add several endpoints here, we need /dogs?number=5 here. The general definition of the fake endpoints is

'ENDPOINT' ⇒ Http::response(RESPONSE_PAYLOAD, HTTP_STATUS)

In our case, the request to the /dogs?number=5 endpoint would return a 200 OK status and the foo ⇒ bar payload - but we want dog facts, right?

The last step is to call the API for a matching result once and copy the JSON output to a file inside the tests folder. The JSON can then be decoded inside our test result and will always return the same response.

How do we store this file? Since the test is running inside the Testbench Laravel application during runtime, we don't have access to the file system of it. Moreover, there is little sense in storing package-specific test data inside our Laravel application. We should store this inside a stubs folder in the package's test directory.

Our file structure in the package looks like this:

dog-facts-package
    - config/
    - src/
    - tests/
      - stubs/
        - response_dogs_200.json
      - DogFactsTest.php

We can access the file with the fake response with file_get_contents inside our test.

<?php

namespace BeyondCode\DogFacts\Tests;

use Orchestra\Testbench\TestCase;
use BeyondCode\DogFacts\DogFacts;

class DogFactsTest extends TestCase {

    /** @test */
    public function it_returns_a_collection_with_random_facts() {

        Http::fake([
          'https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?number=5'
          => Http::response(
              json_decode(file_get_contents('tests/stubs/response_dogs_200.json'), true),
              200
            )
        ]);
		
        $facts = DogFacts::getRandomFacts(5);

        $this->assertIsIterable($facts);
        $this->assertCount(5, $facts);
    }
}

This approach helps us to execute our tests independent from any external API. You can read more about the Http::fake() method and HTTP client testing in the documentation.

If you want to learn more about package development, check out our video course.

Learn how to build a PHP Package

Our course teaches you everything about good package design in PHP and Laravel – along with tips on how to use automated tests in your packages and using private packages for your company.

Learn more
Desktop Apps With Electron course