Testing APIs
Introduction
Integration tests are a great way to make sure your application is working end-to-end as expected. Aphiria, with the help of PHPUnit, comes with some great tools to help you send requests to your application and parse the responses. Aphiria uses automatic content-negotiation in your integration tests, which frees you to make assertions using your app's models without worrying about how to (de)serialize data. The integration tests won't actually send a request over HTTP, either. Instead, it creates an in-memory instance of your application, and sends the requests to that. The nice part about not having to go over HTTP is that you don't worry about having to set up firewall rules for testing your application in staging slots.
Note: All the examples here are assuming that you're using the skeleton app, and extending
IntegrationTestCase
. By default, tests will run in thetesting
environment. If you wish to change this, you can by editing theAPP_ENV
value in phpunit.xml.dist.
Sending Requests
Sending a request is very simple:
use App\Tests\IntegrationTestCase;
use Aphiria\Net\Http\HttpStatusCode;
final class BookQueryTest extends IntegrationTestCase
{
public function testQueryYieldsCorrectResult(): void
{
$response = $this->get('/books/search?query=great%20gatsby');
$this->assertStatusCodeEquals(HttpStatusCode::Ok, $response);
$this->assertParsedBodyEquals(new Book('The Great Gatsby'), $response);
}
}
Use the following methods to send requests and get responses:
delete()
get()
options()
patch()
post()
put()
When specifying a URI, you can use either pass just the path or the fully-qualified URI. If you're using a path, the APP_URL
environment variable will be prepended automatically.
$this->get('/books/123');
// Or
$this->get('http://localhost:8080/books/123');
You can pass in headers in your calls:
$this->delete('/users/123', headers: ['Authorization' => 'Basic Zm9vOmJhcg==']);
All methods except get()
also support passing in a body:
$this->post('/users', body: new User('foo@bar.com'));
If you pass in an instance of IBody
, that will be used as the request body. Otherwise, content negotiation will be applied to the value you pass in.
Negotiating Content
You may find yourself wanting to retrieve the response body as a typed PHP object. For example, let's say your API has an endpoint to create a user, and you want to make sure to delete that user by ID after your test:
$response = $this->post('/users', body: new User('foo@bar.com'));
// Assume our application has a CreatedUser class with an ID property
$createdUser = $this->negotiateResponseBody(CreatedUser::class, $response);
// Perform some assertions...
$this->delete("/users/{$createdUser->id}");
Mocking Authentication
Mocking authentication calls in integration tests is easy. Just call actingAs()
in your test and pass in a callback for the call(s) you want to make while authenticating as the desired principal:
use Aphiria\Net\Http\HttpStatusCode;
use Aphiria\Net\Http\StringBody;
use Aphiria\Security\Identity;
use Aphiria\Security\User;
use App\Tests\Integration\IntegrationTestCase;
class UserIntegrationTest extends IntegrationTestCase
{
public function testUpdatingEmail(): void
{
// For the scoped call in actingAs(), we'll authenticate as the input user
$user = new User([new Identity([])]);
$body = new StringBody('foo@bar.com');
$response = $this->actingAs($user, fn () => $this->put('/email', body: $body));
$this->assertStatusCodeEquals(HttpStatusCode::Ok, $response);
}
}
Response Assertions
Response assertions can be used to make sure the data sent back by your application is correct. Let's look at some examples of assertions:
Name | Description |
---|---|
assertCookieEquals() |
Asserts that the response sets a cookie with a particular value |
assertCookieIsUnset() |
Asserts that the response unsets a cookie |
assertHasCookie() |
Asserts that the response sets a cookie |
assertHasHeader() |
Asserts that the response sets a particular header |
assertHeaderEquals() |
Asserts that the response sets a header with a particular value. Note that if you pass in a non-array value, then only the first header value is compared against. |
assertHeaderMatchesRegex() |
Asserts that the response sets a header whose value matches a regular expression |
assertParsedBodyEquals() |
Asserts that the parsed response body equals a particular value |
assertParsedBodyPassesCallback() |
Asserts that the parsed response body passes a callback function |
assertStatusCodeEquals() |
Asserts that the response status code equals a particular value |
assertCookieEquals
// Assert that the response sets a cookie named "userId" with value "123"
$this->assertCookieEquals('123', $response, 'userId');
assertHasCookie
// Assert that the response sets a cookie named "userId"
$this->assertHasCookie($response, 'userId');
assertHasHeader
// Assert that the response sets a header named "Authorization"
$this->assertHasHeader($response, 'Authorization');
assertHeaderEquals
// Assert that the response sets a header named "Authorization" with value "Bearer abc123"
$this->assertHeaderEquals('Bearer abc123', $response, 'Authorization');
assertHeaderMatchesRegex
// Assert that the response sets a header named "Authorization" with a value that passes the regex
$this->assertHeaderMatchesRegex('/^Bearer [a-z0-9]+$/i', $response, 'Authorization');
assertParsedBodyEquals
// Assert that the response body, after content negotiation, equals a value
$this->assertParsedBodyEquals(new User('Dave'), $response);
assertParsedBodyPassesCallback
// Assert that the response body, after content negotiation, passes a callback
$this->assertParsedBodyPassesCallback($response, User::class, fn ($user) => $user->name === 'Dave');
assertStatusCodeEquals
// Assert that the response status code equals a value (can also use an int)
$this->assertStatusCodeEquals(HttpStatusCode::Ok, $response);