Automated API Testing For Your WordPress Site With Codeception

#

I’ve previously written about how we use Codeception to perform automated testing on our WooCommerce site where we sell our plugins. Automated tests give us peace of mind that our checkout still works after updating WooCommerce, other plugins, or WordPress itself. It’s a very important part of our company.

However, as a company selling premium WordPress plugins, there’s a large section of the site that has been left untested, as it wasn’t involved in the ecommerce flow. I’m talking about our API to handle activating and deactivating plugin license keys, providing updates of the plugin zips, and posting support requests to our customer support system.

The API is an extremely important part of the site and our business, but I’d been putting off adding any testing around it for a while. Until recently I needed to make some changes to it and couldn’t bring myself to make them until I knew I had a safety net of test coverage to catch regressions.

I considered writing unit tests but quickly realised I’d be entering into a large refactor in order to make the code testable, that I didn’t have the time for. After doing acceptance tests for the rest of the site it made sense to do the same for the API.

How does that work with Codeception? Isn’t that for automated clicking and stuff? Turns out Codeception has a pretty nifty REST module that makes it possible to perform API testing. Let me take you through the process of getting set up, writing and running tests.

Test Suite Installation

I’ve already installed Codeception from previous work, so I won’t cover it here.

Codeception recommends you install a new test suite specifically for your API tests, and this is as simple as running the following command:

php vendor/bin/codecept generate:suite api

I’ve named the suite ‘api’, and Codeception will now scaffold all the files needed, including a new api.suite.yml config file, and directory for the suite’s tests.

The api.suite.yml file is where we need to add the REST module and configure it with our site URL. Because the delciousbrains.com API is powered by WooCommerce, I’ve added the query string of wc-api=delicious-brains to the WordPress site URL, so the tests will use the API URL by default:

actor: ApiTester
modules:
    enabled:
        - REST
    config:
        REST:
            url: '%WP_URL%/?wc-api=delicious-brains'
            depends: PhpBrowser

Writing Tests

When I wrote the tests for the main site, I used the procedural ‘Cept’ format, but this time I set up my files as classes using the ‘Cest’ format. ‘Cepts’ are written in a procedural style, whereas ‘Cests’ are written in a class format. This was so I could have one test file to represent a specific API endpoint, and have different methods for the various testing scenarios of that endpoint.

The basics of writing an API test involve sending a request to the API and checking the response. The REST module has methods to send GET, POST, PUT, DELETE and PATCH requests, as well as a variety of methods to test the response from the request.

For example, you could test that an endpoint returns a response with the 200 code, with JSON data:

$I->sendGET('/posts', [ 'status' => 'pending' ]);
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();

You can dig deeper into the JSON response to assert if it contains specific data, like an email address:

$I->seeResponseContainsJson( array( ‘success’ => true ) );

Customizing the Actor

When Codeception generated the ‘api’ test suite it also created some files in the tests\_support directory, including an ApiTester.php file, with a class that extends the default \Codeception\Actor class. This is where we can add methods to re-use across our test cases.

For example, our WooCommerce API isn’t a RESTful API with traditional URL routing with verbs such as api.deliciousbrains.com/license/{key}/activate, but instead is using query string parameters to pass the data to the base URL:

https://deliciousbrains.com/?wc-api=delicious-brains&request=activate_licence&licence_key={key}

As mentioned earlier, I’ve set https://deliciousbrains.com/?wc-api=delicious-brains as the REST URL in the api.suite.yml config, so I need to customize the URL passed to the sendGET and sendPOST methods with extra data, but in a way that is reusable so I can call it multiple times for the same endpoint during testing. I added this method to the ApiTester.php file:

public function sendRequest( $endpoint, $args = array(), $post_args = array() ) {
    $args = array_merge( array( 'request' => $endpoint ), $args );

    $args = http_build_query( $args );

    if ( ! empty( $post_args ) ) {
        $this->sendPOST( '&' . $args, $post_args );

        return;
    }

    $this->sendGET( '&' . $args );
}

This means I can easily test specific endpoints with extra data passed to it:

$args = array(
    'licence_key' => 'a9e82788-5b8d-4b02-5f6a-2f6a8aa3eed3',
    'product'     => 'wp-migrate-db-pro',
    'site_url'    => 'example.com',
);

$I->sendRequest( ‘activate_licence’, $args );

Because the tests are run again and again on the same licence key and other types of data, I needed a way to clear out the subscription for that key at the start of each test. The ApiTester.php file is a good place for this type of code too:

public function dontSeeActivationForLicenceKey( $licence_key ) {
    $table = $this->grabPrefixedTableNameFor( 'woocommerce_software_licences' );
    $this->dontseeInDatabase( $table, array( 'licence_key' => $licence_key ) );
}

I also added a custom assertion to make sure the license and subscription is active after running API requests in the tests:

public function seeActivationForLicenceKey( $licence_key, $site_url, $check_active = false ) {
    $licences_table = $this->grabPrefixedTableNameFor( 'woocommerce_software_licences' );
    $this->seeInDatabase( $licences_table, array( 'licence_key' => $licence_key ) );

    $key_id = $this->grabFromDatabase( $licences_table, 'key_id', [ 'licence_key' => $licence_key ] );

    $args = array( 'key_id' => $key_id, 'instance' => $site_url );
    if ( $check_active ) {
        $args['activation_active'] = 1;
    }

    $activations_table = $this->grabPrefixedTableNameFor( 'woocommerce_software_activations' );
    $this->seeInDatabase( $activations_table, $args );
}

Example Test

Here’s an example of the ActivateLicence endpoint test case with tests for an invalid request where the licence key is missing, and a successful license activation:

class ActivateLicenceCest {

    protected $endpoint = 'activate_licence';

    public function noLicenceKeyTest( ApiTester $I ) {
        $I->sendRequest( $this->endpoint );
        $I->seeResponseIsJson();
        $I->seeResponseContainsJson( array( 'errors' => array( 'no_licence_key_arg' => ‘You did not provide a licence_key argument.’ ) ) );
    }

    public function validTest( ApiTester $I ) {
        $args = array(
            'licence_key' => 'a9e82788-5b8d-4b02-5f6a-2f6a8aa3eed3',
            'product'     => 'wp-migrate-db-pro',
            'site_url'    => 'example.com',
        );

        $I->sendRequest( $this->endpoint, $args );
        $I->seeResponseIsJson();
        $I->seeResponseCodeIs( 200 );
        $I->seeResponseContainsJson( array( 'email' => 'joe@example.com' ) );
        $I->seeResponseContainsJson( array( 'is_first_activation' => '0' ) );
        $I->seeActivationForLicenceKey( $args['licence_key'], $args['site_url'] );
    }
}

As with unit testing, the bulk of writing the tests is just going through and writing tests to cover for all the different scenarios in the code.

Considerations

File Downloads

There were a few things I came across whilst writing tests to cover all the endpoints that are worth sharing for future me and anyone else doing the same thing.

When testing the ‘download’ endpoint, responsible for delivering zip files for updates of our plugin to customer websites, I realized that a successful download request doesn’t return JSON but the actual file data. I didn’t want to go as far as to test the file data was 100% correct but I at least wanted to be sure some data was being returned.

Instead of using JSON assertions, I found the best way to check was to interrogate the HTTP header for the response to make sure the file was the correct type and had some contents:

$args = array(
    'licence_key' => 'a9e82788-5b8d-4b02-5f6a-2f6a8aa3eed3',
    'slug'        => 'wp-migrate-db-pro',
    'site_url'    => 'example.com',
);

$I->sendRequest( $this->endpoint, $args );
$I->seeHttpHeader( 'content-disposition', 'attachment' );
$I->seeHttpHeader( 'content-type', 'application/zip' );
$I->seeHttpHeader( 'content-length' );
$I->dontSeeHttpHeader( 'content-length', 0 );

Third Parties

Our API is responsible for posting customer support messages submitted from within the installed plugins to Help Scout, our email support service. It’s a cool setup, which means we can swap out Help Scout for another provider if we so wish, without having to update the plugins and rely on customers actually updating the plugins on their sites.

Testing that part of the API was a little tricky. I needed a way to check our Help Scout mailboxes to see if a dummy support ticket submitted by the test had arrived. It didn’t look like a Codeception module existed for Help Scout, so I built one. This allowed me to check the latest email sent to a mailbox to see if it contained the correct subject and body:

$args = array(
    'licence_key' => 'a9e82788-5b8d-4b02-5f6a-2f6a8aa3eed3',
    'product'     => 'wp-migrate-db-pro',
    'site_url'    => 'example.com',
);

$post = array(
    'email'   => 'joe@example.com',
    'subject' => 'Testing Testing',
    'message' => 'This is a test message set from the API testing suite',
);

// Send the API request
$I->sendRequest( $this->endpoint, $args, $post );
$I->seeResponseIsJson();
$I->seeResponseContainsJson( array( 'success' => 1 ) );
$I->seeHttpHeader( 'Access-Control-Allow-Origin', '*' );

// Check Help Scout for support ticket
$mailbox_id = 12345;
$I->waitForEmailFromSender( $mailbox_id, $post['email'], 10 );
$I->openNextUnreadEmail();
$I->seeInOpenedEmailSubject( $post['subject'] );
$I->seeInOpenedEmailSender( $post['email'] );
$I->seeInOpenedEmailBody( $post['message'] );
$email = $I->getOpenedEmail();
$I->dontHaveEmailEmail( $email );

This is one of the best features of Codeception – you can test against a multitude of systems with custom modules, because full website testing is very rarely restricted to just asserting against the browser and database.

Running the Tests

As part of the work I did to write acceptance tests for the checkout functionality, I put together a bash script to easily run the tests: generate Codeception classes, start up Chromedriver, and run the acceptance tests.

However, now we have three testing suites – the site tests, one for redirects, and now the API one. Sometimes I might want to just run one of the suites or even just one test case, so I made a few modifications:

#!/usr/bin/env bash

REPO_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
export PATH="${REPO_DIR}/vendor/bin:${PATH}"

cd "${REPO_DIR}"
composer install
cd "${REPO_DIR}/tests"

PREFIX="tests/"
SCRIPT="$@"

codecept build

run_acceptance_tests() {
    kill -9 $(pgrep chromedriver)
    chromedriver --url-base=/wd/hub &

    codecept run acceptance $1

    kill -9 $(pgrep chromedriver)
}

if [ -z "$SCRIPT" ]
then
    # Run all test suites
    codecept run api
    codecept run redirects
    run_acceptance_tests
else
    # Run specific suite test
    SCRIPT=${SCRIPT/#$PREFIX}
    SCRIPT_SUITE="${SCRIPT%%/*}"

    if [ $SCRIPT_SUITE = "acceptance" ]; then
        run_acceptance_tests $SCRIPT
    else
        codecept run $SCRIPT_SUITE $SCRIPT
    fi
Fi

Wrapping Up

I’m still yet to hook up these tests to run automatically with Travis CI, but having them available to run locally during development has been so helpful for my workflow. It’s so important to have the peace of mind when making changes, especially as refactors can introduce regressions.

Have you set up automated tests for your API? What did you use to do it? I’m interested in hearing about other WordPress companies using Codeception. Let me know in the comments below.

About the Author

Iain Poulson

Iain is a WordPress and PHP developer from England. He builds free and premium plugins, as well as occasionally blogging about WordPress. Moonlights as a PhpStorm evangelist.