Functional tests for REST interfaces

Up until now, you have seen what's already been implemented, what is possible to do out of the box, and some additional functionalities like the fixtures.

Now let's have a look at what testing a REST interface entails; the default functional tests available in Codeception are executed by PHPBrowser, and the interface exposed to interact with it is quite limited and can only be used to deal and interact with the markup output by the web server. The REST module provided by Codeception is something we would love.

Just to cite a few of the features available, you'll have functions to set and read headers, such as seeHttpHeader() and haveHttpHeader(), and specific methods to call HTTP requests towards our interface, such as sendGET(), sendPUT(), and sendOPTIONS().

Specifically for our interface of the user, our tests will be split into two parts:

  • Tests on the actual functionality—authentication and interaction with the application
  • Some additional tests to ensure that we are exposing the right endpoints

Now, with this in mind, let's start having a look at the configuration part; in the functional.suite.yml file, just add the REST module and configure it as shown in the following code:

# tests/codeception/functional.suite.yml

modules:
    enabled:

      - Filesystem
      - Yii2
      - REST
      - testscodeception\_supportFixtureHelper
    config:
        Yii2:
            configFile: 'codeception/config/functional.php'
        PhpBrowser:
            url: 'http://basic-dev.yii2.sandbox'
        REST:
            url: 'http://basic-dev.yii2.sandbox/v1/'

The last line is quite important, as we will end up making calls by specifying only our endpoint without the need of naming the module base path. Clearly things need to be adjusted accordingly in case you have more than one REST endpoint you need to test.

Now, once again we need to run codecept build in order to get everything ready before starting to run our tests. This command, as already seen, will take all the module's methods and merge them into our actor's class (which in this case is FunctionalTester).

Let's generate our new test file with the following commands:

$ cd tests/
$ ../vendor/bin/codecept generate:cept functional UserAPICept
Test was created in UserAPICept.php

Now that we have the file, we can start implementing our tests:

<?php
// tests/codeception/functional/UserAPICept.php

$I = new FunctionalTester($scenario);
$I->wantTo('test the user REST API'),

We start the file with the initialization of the FunctionalTester and the definition of the scope of our test.

Defining the API endpoints

As it's now time to implement the tests for our API endpoints, we need to define what these will look like and take our architectural decisions if these haven't been taken beforehand.

The basic interaction we want to provide to our clients interacting with our APIs is the ability to retrieve the user information, and modify it with the specific ability to change the password.

The client would normally know only the username and password. Since our update method will leverage on the ID of the user, we need to find a way for the client to get it in advance. Depending on the type of authentication protocol you decide to use, you can decide to return it right after the authentication has happened, otherwise you need to find a different way.

As you will later see, you're going to use the simplest of the authentication methods available, that is HTTP Basic Auth, which means that all our requests require a username and password to be sent along with them in a header. By doing so we clearly can't return the user ID in the response as this should contain the answer to the call and not the authentication header, so we can decide to provide a "search by username" endpoint. This will clearly make the username a unique field in the database, but that's not an issue, rather it's something you need to take into consideration if you're providing a user creation interface.

Now, we have the following endpoints to test:

  • GET users/search/<username>: This is used to retrieve the ID of the user.
  • GET users/<id>: This is used to retrieve any other information associated with the user.
  • PUT users/<id>: This is used to update the password.

Implementing the tests for the API

As our passwords are passed as encrypted in the fixtures, we need to hardcode them in the tests, in order to authenticate appropriately.

This is not a good practice as we are going to make things a bit harder to maintain. On the other end, if things get more complex, we might want to refactor the code and find a better, more unified solution:

$userFixtures = $I->getFixture('user'),
$user = $userFixtures['basic'];
$userPassword = 'something';

Now that we have some basic information about the user, we can try to grab its ID and check if its authentication works altogether:

$I->amGoingTo('authenticate to search for my own user'),
$I->amHttpAuthenticated($user['username'], $userPassword);
$I->sendGET('users/search/'.$user['username']);

The first step is to prepare the request, which is composed of the Authorization header and the actual request. We don't need to explicitly generate the Authorization header, as we have an abstraction over it provided by amHttpAuthenticated(), which would do that for us.

The header is then sent alongside the GET request over our endpoint; note how the URL omits the /v1/ part that we would normally use to prefix the API:

$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContains($user['username']);
$I->seeResponseContains('password'),
$I->seeResponseContains('id'),

Once we've sent the request, we can start analyzing the response and do various assertions on it:

$userId = $I->grabDataFromJsonResponse('id'),

Finally, we grab the user ID from the response, so we can reuse it afterwards.

The next step is about fetching the user's own information knowing their ID, which looks particularly straightforward to implement:

$I->amGoingTo('ensure I can fetch my own information while being authenticated'),
$I->amHttpAuthenticated($user['username'], $userPassword);
$I->sendGET('users/'.$userId);
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContains($user['username']);
$I->seeResponseContains('password'),
$I->seeResponseContains('id'),

As the last step, we have kept the tests on updating the password and ensuring that the new password works as expected:

$I->amGoingTo('update my own password'),
$I->amHttpAuthenticated($user['username'], $userPassword);
$newPassword = 'something else';
$I->sendPUT(
    'users/' . $userId,
    ['password' => $newPassword, 'authkey' => 'updated']
);
$I->seeResponseIsJson();
$I->seeResponseContains('true'),
$I->seeResponseCodeIs(200);

$I->amGoingTo('check my new password works'),
$I->amHttpAuthenticated($user['username'], $newPassword);
$I->sendHEAD('users/'.$userId);
$I->seeResponseIsJson();
$I->seeResponseContains($user['username']);
$I->seeResponseCodeIs(200);

Note

Please note that due to the length of the tests, we will be keeping them all in one file as it won't affect their legibility, but you can clearly split them in more CEST files to aggregate them in a more concise and logical way.

This should be all you really need to know. We can check that none of the tests will pass at this point, and at the end of the chapter, we will ensure that all of them are finally passing.

Note

Also note that it's not necessary to call amHttpAuthenticated() to send the authentication header every time as it will be cached after the first call in the CEPT file, and should only be required when the header needs to be updated.

Now that we have seen how easy it is to write a functional test, I can leave the creation of additional tests to you. If you want, you can start by checking that the rest of the interfaces have not been exposed, such as the ability to request the list of all users and retrieve or change their passwords.

In the following section of this chapter, we are going to focus on the implementation side of the things by looking at some new, shiny features provided by Yii 2.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset