visit
Am I proud of this montage? You bet I am (by the )
is a specification intended to describe RESTful APIs in JSON and YAML, with the aim of being understandable by humans and machines alike.OpenAPI definitions are language-agnostic and can be used in a lot of different ways:An OpenAPI definition can be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.– In this article, we will see how to combine OpenAPI 3.0.x definitions with integration tests to validate whether an API behaves the way it's supposed to, using the package.We will do so in a fresh installation, for which we'll also generate a documentation using the package.
I will first elaborate a bit further on why this is useful, but if you're just here for the code, you're welcome to skip ahead and go to the A Laravel example section straight away.
By the
The OpenAPI definition describes the API, and the tests use the OpenAPI definition to make sure the API actually behaves the way the definition says it does.All of a sudden, our OpenAPI definition becomes a reference for both our code and our tests, thus acting as the API's single source of truth.$ composer create-project --prefer-dist laravel/laravel openapi-example "8.*"
$ cd openapi-example
$ composer require --dev osteel/openapi-httpfoundation-testing
$ composer require darkaonline/l5-swagger
To make sure Swagger PHP doesn't overwrite the OpenAPI definition, let's set the following environment variable in the
.env
file at the root of the project:L5_SWAGGER_GENERATE_ALWAYS=false
Create a file named
api-docs.yaml
in the storage/api-docs
folder (which you need to create), and add the following content to it:This is a simple OpenAPI definition describing a single operation – a
GET
request on the /api/test
endpoint, that should return a JSON object containing a required foo
key.Let's check whether Swagger UI displays our OpenAPI definition correctly. Start PHP's development server with this
artisan
command, to be run from the project's root:$ php artisan serve
Open in your browser and replace
api-docs.json
with api-docs.yaml
in the navigation bar at the top (this is so Swagger UI loads up the YAML definition instead of the JSON one, as we haven't provided the latter).Hit the enter key or click Explore – our OpenAPI definition should now be rendered as a Swagger UI documentation:
Expand the
/test
endpoint and try it out – it should fail with a 404 Not Found
error, because we haven't implemented it yet.Let's fix that now. Open the
routes/api.php
file and replace the example route with this one:Route::get('/test', function (Request $request) {
return response()->json(['foo' => 'bar']);
});
Time to write a test! Open
tests/Feature/ExampleTest.php
and replace its content with this one:Let's unpack this a bit. For those unfamiliar with Laravel,
$this->get()
is a test method provided by the trait that essentially performs a GET
request on the provided endpoint, executing the request's lifecycle without leaving the application. It returns a response that is identical to the one we would obtain if we'd perform the same request from the outside.We then create a validator using the
Osteel\OpenApi\Testing\ResponseValidatorBuilder
class, to which we feed the YAML definition we wrote earlier via the fromYaml
static method (the storage_path
function is a helper returning the path to the storage
folder, where we stored the definition).Had we had a JSON definition instead, we could have used the
fromJson
method; also, both methods accept YAML and JSON strings respectively, as well as files.The builder returns an instance of
Osteel\OpenApi\Testing\ResponseValidator
, on which we call the get
method, passing the path and the response as parameters ($response
is a Illuminate\Testing\TestResponse
object here, which is a wrapper for the underlying HttpFoundation object, which can be retrieved through the baseResponse
public property).The above is basically the equivalent of saying I want to validate that this response conforms to the OpenAPI definition of a
GET
request on the /test
path.It could also be written this way:$result = $validator->get('/test', $response->baseResponse);
That's because the validator has a shortcut method for each of the HTTP methods supported by OpenAPI (
GET
, POST
, PUT
, PATCH
, DELETE
, HEAD
, OPTIONS
and TRACE
), to make it simpler to test responses for the corresponding operations.Note that the specified path must exactly match one of the OpenAPI definition's .You can now run the test, which should be successful:$ ./vendor/bin/phpunit tests/Feature
Open
routes/api.php
again, and change the route for this one:Route::get('/test', function (Request $request) {
return response()->json(['baz' => 'bar']);
});
Run the test again; it should now fail, because the response contains
baz
instead of foo
, and the OpenAPI definition says the latter is expected.Our test is officially backed by OpenAPI!The above is obviously an oversimplified example for the sake of the demonstration, but in a real situation a good practice would be to overwrite the
MakesHttpRequests
trait's method, so it performs both the test request and the OpenAPI validation.As a result, our test would now be a single line:$this->get('/api/test');
This could be implemented as a new
MakesOpenApiRequests
trait that would "extend" the MakesHttpRequests
one, and that would first call the parent call
method to get the response. It would then work out the path from the URI, and validate the response against the OpenAPI definition before returning it, for the calling test to perform any further assertions as needed.While the above setup is a great step up in improving an API's robustness, it is no silver bullet; it requires that every single endpoint is covered with integration tests, which is not easily enforceable in an automated way, and ultimately still requires some discipline and vigilance from the developers. It may even feel a bit coercive to some at first, since as a result they are basically forced to maintain the documentation in order to write successful tests.
The added value, however, is that said documentation is now guaranteed to be much more accurate, leading to happy consumers who will enjoy an API which is less likely to act erratically; this, in turn, should lead to less frustrated developers, who shall spend less time hunting down pesky discrepancies.All in all, making OpenAPI definitions the single source of truth for both the API documentation and the integration tests is in itself a strong incentive to keep them up to date; they naturally become a priority, where they used to be an afterthought.As for maintaining the OpenAPI definition itself, doing so manually can admittedly feel a bit daunting. Annotations are a solution, but I personally don't like them and prefer to maintain a YAML file directly. IDE extensions like make it much easier, but if you can't bear the sight of a YAML or JSON file, you can also use like to do it through a more user-friendly interface.And since we're talking about Stoplight*, by is a good starting point for API documentation in general, and might help you choose an approach to documenting that suits you.* I am not affiliated with Stoplight in any way
This story was originally published on .