Get Started Testing a Yii2 API Using Codeception

April 28, 2021
Written by
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Twilion

Get started testing a Yii2 API using Codeception

Testing (and TDD by extension) has drummed up more than its fair share in terms of significant debates. While proponents and opponents may disagree on specifics, they always come to one conclusion: it is unprofessional to ship untested code. Testing provides a level of assurance, not only that the code meets the provided specification but that it can handle edge cases that may be encountered upon deployment.

In this article, I will show you how to test a RESTful API built with the Yii framework (version 2)usingCodeception. Built on PHPUnit, Codeception provides support for creating and running unit, functional, and acceptance tests on PHP applications in a unified manner.

To allow us to focus on the key aspects of testing in this tutorial, we’ll be building on the result of a previous tutorial where Yii2 was used to build a secure RESTful API. You're, however, very free to use another Yii2 project, if you have one.

Prerequisites

To complete this tutorial, you’ll need the following:

  • PHP 7.4 (or above)
  • Composer globally installed
  • A MySQL/MariaDB database

Getting started

To get started, clone the Yii2 project from the Github repository, using the command below.

git clone https://github.com/yemiwebby/yii-library-api.git

The command will clone the project into a new folder named yii-library-api. Once the code has been cloned, change into the new project folder and install all its required dependencies using Composer. To do that, run the commands below.

cd yii-library-api
composer install

Next, using your preferred database management application, create a new MySQL database called yii_library_api.Then, set the database connection parameters to link your application with the newly created database. To do this, open config/db.php and update the returned array to match the one shown below, but replace the three placeholders with the applicable details for your database.

return [
    'class' => 'yii\\db\\Connection',
    'dsn' => 'mysql:host=<HOSTNAME>;dbname=yii_library_api',
    'username' => '<USERNAME>',
    'password' => '<PASSWORD>',
    'charset' => 'utf8',
];

The GitHub repo also contains some migrations which will create the tables and seed the database with some initial data. Run them using the command below.

php yii migrate

When prompted, type yes and press Enter to run the migrations. When the migrations have been run, open the yii_library_api database in your preferred application to see the new table structure. It should have the following tables:

  • book
  • loan
  • member
  • migration

The book table with sample books is shown below.

Sample listing of the book table

The member table with sample members is shown below.

Sample listing of the member table

At this stage, we have built an application that can handle the following requests:

Method

Route

Description

GET

/{members, books}

List all members or books page by page;

POST

/{members, books}

Create a new member or book;

GET

/{members, books}/\d+

Return the details of the member or book 123;

PATCH and PUT

/{members, books}/\d+

Update the member or book 123;

DELETE

/{members, books}/\d+

Delete the member or book 123;

GET

/loans

Get a list of all the loans available

POST

/loans

Borrow a book by providing the member id and book id

Setting up the test environment

To avoid unwanted side effects as a result of the test cases interacting with the development database, we're going to set up a test database. Using your preferred database management application, create a new database called library_api_test.

Then, update the $db['dsn'] parameter in config/test_db.php to match the code below.

$db['dsn'] = 'mysql:host=localhost;dbname=library_api_test';

Having connected the application to the test database, create the tables, and seed them with data using the command below. Type "yes" when prompted and press "Enter" to run the migrations.

php tests/bin/yii migrate

Once completed, you should have a test database with sample data and the same structure as the development database. Next, in config/test.php, update the urlManager component to match the following code.

'urlManager' => [
            'enablePrettyUrl'     => true,
            'showScriptName'      => false,
            'enableStrictParsing' => true,
            'rules'               => [
                ['class' => 'yii\rest\UrlRule', 'controller' => 'member'],
                ['class' => 'yii\rest\UrlRule', 'controller' => 'book'],
                'POST loans' => 'loan/borrow',
                'GET loans' => 'loan/index'
            ],
        ],

It is important to update the routing rules in the test environment to ensure that our requests are handled as they would be in the development environment. This allows the tests to pass accordingly and in the event that there is a bug in the code, the tests will fail and bring such a bug to light.

Setting up an API test suite

With these in place, you can set up a test suite and write test cases for the API endpoints. Yii2 projects are, by default, bootstrapped with a minimal Codeception which makes it pretty easy to get started quite quickly.

Test cases and related files are located in the tests directory. Functional tests are located in the tests/functional directory. Unit tests are located in the tests/unit directory, and acceptance tests are located in the tests/acceptance directory. Take a few minutes to familiarize yourself with them before moving on.

The tests written in this article will, essentially, be functional tests. The difference between the tests we will write and those in the tests/functional directory is that, instead of testing HTML responses on user actions, the tests will make requests and responses RESTfully.

To start writing API tests, create a suite for them using the command below.

./vendor/bin/codecept generate:suite api

This command creates a Codeception Helper and an Actor to help with testing the endpoints. The suite configuration is also created and saved in the tests directory. Testing a RESTful API with Codeception requires the codeception/module-rest package, install it using the following command.

composer req --dev codeception/module-rest

Next, update the API suite configuration. Do that by opening tests/api.suite.yml and updating it to match the configuration below.

actor: ApiTester
modules:
    enabled:
        - REST:
            url: http://localhost:8080/index-test.php/
            depends: Yii2
            part: JSON
            configFile: 'config/test.php'
        - \Helper\Api

    config:
        - Yii2

Next, build the test suite by running the following command.

php vendor/bin/codecept build

With the test suite in place, let's create our test classes (Cests). Create three new Cests to test the endpoints for Members, Books, and Loans, using the following commands.

./vendor/bin/codecept generate:cest api Member
./vendor/bin/codecept generate:cest api Book
./vendor/bin/codecept generate:cest api Loan

Testing the endpoints

To run the tests in the API suite, run the following command.

php vendor/bin/codecept run api

The output should be similar to that in the screenshot below.

Output of the API tests

Because we're going to run the tests quite often, let's create an alias for the command which is easier to type. To do that, open composer.json and add the following to the scripts element.

"test-api": "php vendor/bin/codecept run api"

Now, you can run your tests by typing the following command.

composer test-api

Writing test cases

A key selling point for Codeception is the fact that the tests are very descriptive. Functions that serve as actions (in this case, sending a request) are prefixed with a verb, such as send”. Assertions (functions that evaluate the outcome of the triggered action) are prefixed with “see” or “dontSee”. If some data from the response to the action is required for follow-up testing, Codeception provides 'grabber' functions which are prefixed with “grab”.

Each test case is a function which takes an ApiTester object (which is anActor) as a parameter. The ApiTester provides access to the functions described above.

Testing the members endpoints

The first test we will write is for the /members endpoint. When a request is sent to this endpoint, an HTTP OK (200) response is expected to be returned along with a JSON array of objects corresponding to the saved members. Each object should contain the following three things:

  • The member's id, which is an integer
  • The member's name, which is a string
  • A string representation of the member's start date

Open tests/api/MemberCest.php and add the following code after the tryToTest method.

public function getAllMembers(ApiTester $I) {
        $I->sendGet('members');
        $I->seeResponseCodeIs(HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseIsValidOnJsonSchemaString('{"type":"array"}');
        $validResponseJsonSchema = json_encode(
            [
                'properties' => [
                    'id'         => ['type' => 'integer'],
                    'name'       => ['type' => 'string'],
                    'started_on' => ['type' => 'string']
                ]
            ]
        );
        $I->seeResponseIsValidOnJsonSchemaString($validResponseJsonSchema);
    }

In the same vein, we can write a test to validate the response when we try to get the details of a single member. Because we have seeded the database with at least one user, we can run the following test. Add the following code to tests/api/MemberCest.php after the code that you just added.

public function getMember(ApiTester $I) {

    $I->sendGet('members/1');
    $I->seeResponseCodeIs(HttpCode::OK);
    $I->seeResponseIsJson();
    $I->seeResponseIsValidOnJsonSchemaString('{"type":"object"}');
    $validResponseJsonSchema = json_encode(
        [
            'properties' => [
                'id'         => ['type' => 'integer'],
                'name'       => ['type' => 'string'],
                'started_on' => ['type' => 'string']
            ]
        ]
    );
    $I->seeResponseIsValidOnJsonSchemaString($validResponseJsonSchema);
}

Next, write a test to validate the response when a valid request is made to create a member. Add the following code to tests/api/MemberCest.php after the code that you just added.

public function createNewMember(ApiTester $I) {

        $faker = Factory::create();
        $I->sendPost(
            'members',
            [
                'name'       => $faker->name,
                'started_on' => $faker->date('Y-m-d H:i:s')
            ]
        );
        $I->seeResponseCodeIs(HttpCode::CREATED);
        $I->seeResponseIsJson();
        $I->seeResponseMatchesJsonType(
            [
                'id'         => 'integer',
                'name'       => 'string',
                'started_on' => 'string',
            ]
        );
    }

Similar to the getMember test function, this test case evaluates the structure of the returned object, albeit with another function provided by Codeception (seeResponseMatchesJsonType) to do this.

To test the update endpoint, add the following code to tests/api/MemberCest.php after the code that you just added.

public function updateMember(ApiTester $I) {

        $faker = Factory::create();
        $newName = $faker->name;
        $I->sendPatch(
            'members/1',
            [
                'name' => $newName
            ]
        );
        $I->seeResponseCodeIs(HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseContainsJson(['name' => $newName]);
    }

Finally, to test the endpoint responsible for deleting members, add the following code to tests/api/MemberCest.php after the code that you just added.

public function deleteMember(ApiTester $I) {

        $I->sendDelete('members/1');
        $I->seeResponseCodeIs(HttpCode::NO_CONTENT);
                                //try to get deleted user
        $I->sendGet('members/1');
        $I->seeResponseCodeIs(HttpCode::NOT_FOUND);
        $I->seeResponseIsJson();
    }

Note: Don't forget to add the following imports:

use Codeception\Util\HttpCode;
use Faker\Factory;

At this point, we can run our test suite using our composer test-api shortcut. Your response should be similar to the screenshot shown below.

Output of the second run of the API tests

Testing the oans endpoints

For the loan endpoints, we will add two tests. The first test will ensure that the system behaves appropriately when a book is borrowed. The second test will ensure that the system does not allow a book to be double-borrowed i.e., a book that has already been borrowed cannot be borrowed again.

Open tests/api/LoanCest.php and add the following code for the first test case after the tryToTest method.

public function borrowBook(ApiTester $I) {
        $I->sendPost(
            'loans',
            [
                'book_id'   => 1,
                'member_id' => 1,
            ]
        );
        $I->seeResponseCodeIs(HttpCode::OK);
        $I->seeResponseIsJson();
        $I->seeResponseMatchesJsonType(
            [
                'id'                => 'integer',
                'book_id'           => 'string',
                'borrower_id'       => 'string',
                'borrowed_on'       => 'string',
                'to_be_returned_on' => 'string'
            ]
        );
    }

To test that the system doesn't allow double borrowing, add the following test case to tests/api/LoanCest.php after the code that you just added.

public function tryToDoubleBorrowBook(ApiTester $I) {

        $I->sendPost(
            'loans',
            [
                'book_id'   => 1,
                'member_id' => 3,
            ]
        );
        $I->seeResponseCodeIs(HttpCode::OK);
        $I->seeResponseIsJson();
        $I->sendPost(
            'loans',
            [
                'book_id'   => 1,
                'member_id' => 3,
            ]
        );
        $I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
        $I->seeResponseIsJson(
            [
                'error' => 'This book is not available for loan'
            ]
        );
    }

NOTE: Don't forget to add this import:

use Codeception\Util\HttpCode;

Run the tests using the composer test-api command and the results should match the screenshot below.

Output of the third run of the API tests

Conclusion

For such a critical aspect of software development, testing sometimes fails to receive the attention it deserves. Sometimes this may be due to the pressure placed on development by management.

A more common reason however is the difficulty encountered in trying to combine different tools in order to build a suite of unit, functional, and acceptance tests. This is where Codeception really shines!

By making it easy to write test cases in human-readable form, Codeception reduces the burden developers often encounter in trying to describe the expected behavior of the application without writing any code. This means that by taking advantage of Codeception, Test Driven Development becomes easier to understand and implement.

The entire codebase for this tutorial is available on GitHub. Feel free to explore further. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.