This post is part 3 of the “Building an SDK with PHP” series. Read Part 1 and Part 2
In our last article we’ve looked at how we can make our SDK configurable and today we’ll apply this to cover our SDK with several unit tests.
What should we test?
The test suite of our SDK should be focused on the behaviour that it offers. If our SDK offers methods to interact with an HTTP API, then those methods should be tested. In the context of our SDK this means that, given a certain set of parameters, we want to assert the correct HTTP request is being made. However, we do not want our test suite to actually perform an HTTP request.
To solve this, the first thing that comes to mind is that we should mock our HTTP client using PHPUnit’s mocks or Mockery. The downside of that approach is that this would heavily tie our tests to our implementation. If we decide to refactor the internals of our SDK, we would have to refactor our tests as well.
This is where the Mock Client package comes in. This client is essentially an in-memory implementation of an HTTP Client that collects the requests that are performed but also allows faking responses.
Mocking our requests
The first step is to install the Mock Client dependency with composer:
composer require --dev php-http/mock-client
The first test we will write will be a fairly simple one: we will retrieve the current HTTP client on the SDK and try to perform a GET request to a URL.
First of all, we’ll create our own TestCase
implementation that we can extend with our own methods to make common parts easier to execute. In this case, I’m providing a givenSdk()
method that will return an SDK instance:
abstract class TestCase extends \PHPUnit\Framework\TestCase
{
protected function givenSdk(): Sdk
{
return new Sdk();
}
}
The second thing we’ll do is make sure we default to use the Mock Client implementation so we do not accidentally send out a real HTTP request. We can configure this in our PHPUnit’s bootstrap file by adding the following lines:
use Http\Discovery\ClassDiscovery;
use Http\Discovery\Strategy\MockClientStrategy;
require __DIR__.'/../vendor/autoload.php';
ClassDiscovery::prependStrategy(MockClientStrategy::class);
To explain what this does we’ll do a little throwback to part 1 of this series where I explained that PHP-HTTP is able to discover a PSR-18 implementation. The line above makes sure that Mock Client will always be used.
Now that we’ve done preliminary work we can create our first test.
final class SdkTest extends TestCase
{
public function testCanRequest200Response(): void
{
$httpClient = $this->givenSdk()->getHttpClient();
$response = $httpClient->get('/todos');
$this->assertEquals(200, $response->getStatusCode());
}
}
In the test above we’re asserting that the response we get after doing a GET request has a status code of 200, which is the default response returned by Mock Client. This might not always be what you want, but we can leverage Mock Client to customize the response using the addResponse
method.
In order to do this, we’ll need to have access to our actual Mock\Client
instance. A simple way to do this is to add it as a property to our base TestCase
and use that client in our givenSdk
method. This also means we reduce the boilerplate in our actual test cases.
abstract class TestCase extends \PHPUnit\Framework\TestCase
{
protected Client $mockClient;
protected function setUp(): void
{
parent::setUp();
$this->mockClient = new Client();
}
protected function givenSdk(): Sdk
{
return new Sdk(new Options([
'client_builder' => new ClientBuilder($this->mockClient),
]));
}
}
The actual test would look like this:
public function testCanRequest201Response(): void
{
$this->mockClient->addResponse((new Response())->withStatus(201));
$httpClient = $this->givenSdk()->getHttpClient();
$response = $httpClient->post('/todos');
$this->assertEquals(201, $response->getStatusCode());
}
The mocked response added by addResponse
will only be used for the first request done by the HTTP client. If you need to assert multiple requests you can add additional responses as seen below.
public function testCanRequestMultiple201Responses(): void
{
$this->mockClient->addResponse((new Response())->withStatus(201));
$this->mockClient->addResponse((new Response())->withStatus(201));
$httpClient = $this->givenSdk()->getHttpClient();
$this->assertEquals(201, $httpClient->post('/todos')->getStatusCode());
$this->assertEquals(201, $httpClient->post('/todos')->getStatusCode());
}
While this approach would cover a lot of use cases in your tests, there might be cases where you want to have some more control and specific mock responses based on the request. To do this we can make use of the on
method that takes a RequestMatcher
and the Response
as it’s argument.
In the following example we’ll mock the response for a 404 endpoint so that it returns a 404 status code. But we’ll do this only once during our test setup:
public function testCanHandle404s(): void
{
$this->mockClient->on(new RequestMatcher('404'), (new Response())->withStatus(404));
$httpClient = $this->givenSdk()->getHttpClient();
$this->assertEquals(200, $httpClient->get('/todos')->getStatusCode());
$this->assertEquals(404, $httpClient->get('/404')->getStatusCode());
$this->assertEquals(404, $httpClient->get('/404')->getStatusCode());
$this->assertEquals(404, $httpClient->get('/404')->getStatusCode());
}
Conclusion
We’ve seen how you can leverage PSR-18 and the php-http/mock-client implementation to improve tests that rely on HTTP requests so that they are more easy to read and maintain. While this was done in the context of building an SDK, you can apply the same techniques to an application.
As always, you can see the current status of this example SDK that we’ve been building on GitHub.
Member discussion