Let’s make an API module that will contain the actions currently reachable by the /services/json and /services/yaml routes.

The following use cases should be supported:

  • The GET request to the /api/services/json route should return the list of attributes of all registered services in the JSON format
  • The GET request to the /api/services/yaml route should return the list of attributes of all registered services in the YAML format

We define result in JSON format as the string that can be converted to the PHP data structures without error using the yii\helpers\Json::decode() method.

We define the result in YAML format as the string that can be converted to the PHP data structures without error using the Symfony\Component\Yaml\Yaml::parse() method.

Building a test suite to support testing the API module

We will not make a REST-compliant API here. We will get just the minimal reader API to provide us with the list of records registered in the database. However, to use the end-to-end acceptance tests as we did all the time until now is an overkill in this case. We need some simple way to perform the following actions:

  • Install some known data to the database
  • Poll the API endpoint for the data
  • Ensure the data returned is indeed a serialized representation of the data from the database

Codeception, which we are using as our test harness, has a limitation here. On one hand, it has the REST module that provides exactly what we need: the sendGET(), canSeeResponseCodeIs(), and grabResponse() methods (their names tell it all). On the other hand, the REST module cannot be used together with the WebDriver module that our acceptance test suite is using.

Let’s just create a separate test suite just for testing API endpoints. Run the following autogenerator:

$ ./cept generate:suite api ApiTester

We have the tests/api directory and the tests/api.suite.yml configuration now.

To properly configure this suite, we should understand that these tests will make requests to the web server serving the application, but we must have a connection to the database to be able to write the data we control. So, we have to run the test suite from within the deploy target machine, such as the functional tests. However, unlike the functional tests, the API test suite requires that the full application environment together with the web server is up and available, so it’s more like the end-to-end tests in this regard. This will be quite tricky to implement on a testing server.


It is obvious now that we wrapped ourselves into the tight mess of the high-level tests that require the runtime environment to be prepared and the application deployed and running. This setup is slow to run and hard to implement reliably. This is because we rely too much on the Yii machinery in our code, having stopped decoupling from the framework for simplicity in Chapter 3, Automatically Generating the CRUD Code. As a rude workaround, we could completely drop the requirement of having the Yii application up and running by testing the controller action methods directly, as they return the result of their work, and not (for example) print the result directly to STDOUT. However, this will make us unable to see integration problems with Yii in a production environment. In fact, you should have the multilayered test harness, spanning from unit tests of the business rules up to the end-to-end acceptance tests using the UI in an abstract way. To restrict the span of this book, we are showing only the highest-level tests for you here. Fine-grained refactoring was omitted for the same reasons. Although its name has two components, this book is more about Yii 2 and less about the web development in general after all.

Knowing that we’ll run the API tests on the deploy target, and therefore call the web server by the loopback interface, we are now able to properly configure the API test suite by means of tests/api.suite.yml:

class_name: ApiTester
      - ApiHelper
      - PhpBrowser
      - REST
      - Db
            url: 'http://localhost'
            url: 'http://localhost'

Let’s discuss the highlighted lines from top to bottom:

  • The REST module requires the PhpBrowser module as the transport provider, so it’ll be able to actually send requests to the application.
  • We need the Db module to reset the database after each test run, as we’ll autogenerate the data to be present in the database.
  • The PhpBrowser module forces us to provide the base URL for the application. This will be the loopback interface at the same machine. The web server must be running and be serving our application.
  • The REST module should have the means to access the application, so we provide the base URL for it. In our case it’s clearly a code duplication, which is unavoidable. This is not so in a general case, as the REST module can be configured to access some specific subroute defined to be the API endpoint, and thus all requests routes specified in our tests can be shortened by this API endpoint prefix.

As we’re using the Db module, we need to configure it to be able to access our database. We already did this for the functional test suite, in the tests/functional.suite.yml file. So, to avoid code duplication, we can just move the whole modules.config.Db section from the tests/functional.suite.yml file to the global codeception.yml file at the root of the code base. However, if your functional suite is configured to be connected to a database that is different from the one your application is using, then you should not do this and instead configure the Db module in the API suite individually.

This is not all. We need to instantiate the Yii application for our test cases to be able to utilize the database connection from application classes. We’ll do it inside the generated tests/api/_bootstrap.php file in the same way it was done for the functional test suite:

require_once(__DIR__ . '/../../vendor/autoload.php');
require_once(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
new yii\web\Application(
    require(__DIR__ . '/../../config/web.php')

We’re creating a web application again.


Do not forget that in the case of API, as well as acceptance suites the instance of yii\web\Application and the one that we made requests to are completely separate entities and you cannot hope to share data between them in any way except the filesystem and the database (or maybe some other trick from the domain of crazy professional workarounds). Be very careful when configuring the API suite that the Db module in Codeception and the application itself will be talking with the same database!

Don’t forget to generate a tester class for the API test suite by running the build command:

$ ./cept build

Defining the requirements for automatic tests of API modules

Now, the preparations are complete and we can create the actual test script:

$ ./cept generate:test api ServicesListApi

We’re essentially making the API for managing the list of services, after all.

It’ll not be the cept-style test, written as a prose, but a normal test case that is written as a PHP class. That’s how the requirements can be defined as a Codeception test:

    /** @test */
    public function ReturnsValidJson()
        $expectedData = [];
        $expectedData[0] = $this->registerService();
        $expectedData[1] = $this->registerService();


        $response = $this->tester->grabResponse();
        $responseData = \yii\helpers\Json::decode($response);

        $this->assertInternalType('array', $responseData);
        $this->assertEquals($expectedData[0], $responseData[0]);
        $this->assertEquals($expectedData[1], $responseData[1]);

For the sake of simplicity, we are going for relatively large steps here. We register two services in the database to test the ability of our code to deal with nontrivial datasets.

Then, we send the GET request to our predefined API endpoint and check whether we get the JSON response with the data we have just saved to the database.

The test for the YAML endpoint will be exactly the same, except the API endpoint will end in yaml instead of json and the decoding will be done by a different class. So instead of the line:


We will use the following line:


Also, instead of using the line:

        $responseData = \yii\helpers\Json::decode($response);

We will use the line:

        $responseData = \Symfony\Component\Yaml\Yaml::parse($response);

However, before that, let’s define the smoke test to check whether we actually have the endpoints we want:

    /** @test */
    public function HasJsonEndpoint()
        $response = $this->tester->grabResponse();

        $this->tester->canSeeResponseCodeIs(200); //1
        $this->assertNotEquals('', $response); //2

We’re asserting the following points:

  • The response code is HTTP 200 OK
  • The response body is not empty (it should never be empty as the Yii error handler sends quite a long HTML body for error pages)

The same applies for the HasYamlEndpoint() test.

Before we make the actual module, let’s deal with the ServicesApiTest.registerService() helper method we glossed over. The idea behind the reader API is that it returns a plain JavaScript object, a set of key-value pairs. So, we certainly do not want the full serialized yii\db\ActiveRecord returned from there, but rather just its attributes. Given all that, we define the following helper method:

    private function registerService()
        $service = $this->imagineService();


        return $service->attributes;