Adonis.js is one of the fastest-growing Node.js frameworks. As stated on its homepage, the framework is designed for fans of test-driven development (TDD). As a feature of this design, it comes bundled with a specialized testing framework.

In this tutorial, you will learn how to automate the testing of an Adonis.js API so that your tests will run on every change to your codebase.

Prerequisites

To follow this tutorial, a few things are required:

  1. Basic knowledge of Javascript
  2. Node.js installed on your system (>= 8.0)
  3. Adonis.js CLI installed globally
  4. A CircleCI account
  5. A GitHub account

With all these installed and set up, let’s begin the tutorial.

Cloning the Adonis.js API project

To get started, you need to clone the API project that we will be testing. In your terminal, run the following command to clone the base project:

git clone --single-branch --branch base-project https://github.com/CIRCLECI-GWP/adonis-api-testing.git

This will immediately clone the base-project branch of the repository into the location in which you have run the above command. This is the starting point of the API project. It has no tests setup.

Next, go into the root of the project and install the required dependencies:

cd adonis-api-testing
npm install

Then, create a new file at the root of the project with the name .env (note the dot .), and paste in the following configuration:

HOST=127.0.0.1
PORT=3333
NODE_ENV=development
APP_NAME=AdonisJs
APP_URL=http://${HOST}:${PORT}
CACHE_VIEWS=false
APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=adonis
HASH_DRIVER=bcrypt

The above config specifies that we are in a development environment and that we will be using a SQlite database named adonis for persistent storage. To get our database and database schema setup, run the Adonis.js project migrations with the following command:

adonis migration:run

Migrations Run

We have now completely set up our application and can take it for a spin. Boot up the application by running the following command:

adonis serve --dev

This command will start the API running at http://127.0.0.1:3333. The port might be different if 3333 is already in use on your system.

Our API is a simple User accounts API. As seen in the ./start/routes.js file, it contains two main endpoints.

// start/routes.js

"use strict";

const Route = use("Route");

Route.get("/", () => {
  return { greeting: "Welcome to the Adonis API tutorial" };
});

//User api routes
Route.group(() => {
  Route.post("create", "UserController.create");

  Route.route("get", "UserController.fetch", ["GET", "POST"]);
}).prefix("user");

The create route calls the create method of UserController to create a new user by supplying it with a username, email, and password. We also have a get endpoint that simply calls the fetch function of UserController to return an array of users in the database. The implementation of these controller functions can be found in the app/Controllers/Http/UserController.js file shown below.

// app/Controllers/Http/UserController.js

async create({ request, response }) {
    const data = request.post();

    const rules = {
        username: `required|unique:${User.table}`,
        email: `required|unique:${User.table}`,
        password: `required`
    };

    const messages = {
        "username.required": "A username is required",
        "username.unique": "This username is taken. Try another.",
        "email.required": "An Email is required",
        "email.unique": "Email already exists",
        "password.required": "A password for the user"
    };

    const validation = await validate(data, rules, messages);

    if (validation.fails()) {
        const validation_messages = validation.messages().map((msgObject) => {
            return msgObject.message;
        });

        return response.status(400).send({
            success: false,
            message: validation_messages
        });
    }

    try {
        let create_user = await User.createUser(data);

        let return_body = {
            success: true,
            details: create_user,
            message: "User Successully created"
        };

        response.send(return_body);
    } catch (error) {
        Logger.error("Error : ", error);
        return response.status(500).send({
            success: false,
            message: error.toString()
        });
    }
} //create


async fetch({ request, response }) {
    const data = request.all();

    try {
      const users = await User.getUsers(data);

      response.send(users);
    } catch (error) {
      Logger.error("Error : ", error);
      return response.status(500).send({
        success: false,
        message: error.toString(),
      });
    }
} //fetch

Testing the API with Postman

Let’s now put our API to the test by calling our endpoints. We will be using Postman to test our endpoints. Make sure that your app is running. If not, run adonis serve --dev once again.

Testing user creation

User Creation - Succesfull

Testing user fetch

User Fetch

Setting up the testing framework

The next step is to set up the testing framework for testing Adonis.js applications. Fortunately, Adonis.js has its own specialized testing package known as Vow. Install Vow with the following command:

adonis install @adonisjs/vow

The installation of this package will cause the following changes to your project:

  • A vowfile.js file will be created at the root of your project.
  • A test folder will be created at the root of the project. This is where all of the tests will be contained. In order to organize tests, all unit tests are placed inside a unit folder within this folder. Functional tests are placed inside a functional folder. By default, a sample unit test suite is created inside test/unit/example.spec.js.
  • A .env.testing file is created at the root of the project to contain environment variables specific to testing purposes. This file gets merged with .env, so you only need to define values you want to override from the .env file.

Replace the content of .env.testing with the configuration below:

HOST=127.0.0.1
PORT=4000
NODE_ENV=testing
APP_NAME=AdonisJs
APP_URL=http://${HOST}:${PORT}
CACHE_VIEWS=false
APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=adonis
HASH_DRIVER=bcrypt

The final step to complete our test setup is to add the Vow provider to our project. In the ./start/app.js file, add the following item to the aceProviders array:

...

const aceProviders = [
  ....,
  "@adonisjs/vow/providers/VowProvider"
];

...

Run the sample test that came with the Vow installation by running the following command:

adonis test

The sample test will run successfully.

Sample Tests

Adding tests

It is now time to start adding some proper tests to our API project. We will be adding tests to test our create and get endpoints. Run the following command to create a test suite in which we will put our tests:

adonis make:test User

In the options that display after running this command, select Functional test (using the arrow keys) and press Enter.

A new file for the test suite will be automatically created at test/functional/user.spec.js. In this file, replace the contents with the following code:

// test/functional/user.spec.js

"use strict";

const { test, trait, after } = use("Test/Suite")("User");

const User = use("App/Models/User");

trait("Test/ApiClient");

const randomString = generateRandomString();

test("Test User creation", async ({ client }) => {
  const userData = {
    username: randomString,
    email: `${randomString}@test.com`,
    password: "123456"
  };

  const response = await client.post("/user/create").send(userData).end();

  response.assertStatus(200);
}).timeout(0);

test("Fetch all users", async ({ client }) => {
  const response = await client.get("/user/get").end();

  response.assertStatus(200);
  response.assertJSONSubset([
    {
      username: randomString,
      email: `${randomString}@test.com`
    }
  ]);
}).timeout(0);

//Delete the created user
after(async () => {
  await (await User.findBy("username", randomString)).delete();
});

function generateRandomString(length = 7) {
  var result = "";
  var characters =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  var charactersLength = characters.length;
  for (var i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}

In the above test suite, the Adonis.js Test/ApiClient trait is used to gain access to the client object, which is used to call our endpoints. The generateRandomString random number generator function is used to create a fake username and email for a test user.

In the Test User creation test, we create a new user by calling the create endpoint with the appropriate data, and checking if we get a 200 response code that indicates that the operation was successful.

In the Fetch all users test, the client object is used to call our get endpoint. The response is then tested to ensure that it returns a 200 status code. We also check to see if it returns an array containing the user we created in the previous test.

We attach .timeout(0) to each test to override the timeout limit for the test runner as functional and browser tests might take a while to run.

Finally, we do some clean up by deleting the test user we created. This is done after both tests have completed.

Time to run all the tests contained in the tests folder. To do that, run the test command once again:

adonis test

All Tests Run

Connecting the API project to CircleCI

Our next task is to get our project set up on CircleCI. First, you need to initialize your Adonis.js project as a git repository.

Then, create a remote GitHub repository for the project, commit your changes (if any), and push the local project to the remote repository.

Next, go to the Add Projects page on the CircleCI dashboard to add the project.

Add Project - CircleCI

Click Set Up Project.

Add Config - CircleCI

On the setup page, click Add Manually to instruct CircleCI that we would be adding a configuration file manually and not using the sample displayed. Next, you get a prompt to either download a configuration file for the pipeline or start building.

Build Prompt - CircleCI

Click Start Building to begin the build. This build will fail because we have not set up our configuration file yet, this we will do later on.

Automating our tests

Now that we have our project connected to CircleCI, we can write a configuration for our continuous integration (CI) pipeline that will automate our testing process.

At the root of your the project, create a folder named .circleci, and a file within it named config.yml. Inside the config.yml file, enter the following code:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: circleci/node:11-browsers
    steps:
      - checkout
      - run:
          name: Update NPM
          command: "sudo npm install -g npm"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: Install Dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Run Migrations
          command: |
            mv .env.testing .env
            node ace migration:run
      - run:
          name: Run tests
          command: npm run test

In the configuration file above, we start by updating npm to ensure that we are using the lastest version. Next, we install the required dependencies and cache them. We then create our environment configuration file (.env) and run our migrations. With all dependencies and database set up, we run our tests.

Save this file, commit, and push your changes to your repository to trigger the CI pipeline to run.

Build Successful - CircleCI

Let’s view our test results by clicking on the build to see behind the scenes.

Tests Automated - CircleCI

Our tests are now running automatically when we push new code to our repository.

Conclusion

Test-driven development (TDD) is a best practice that a lot of developers are still struggling to integrate into their development flow. Having a framework that has TDD built into its core really makes adopting it less cumbersome. In this tutorial, we learned to set up the testing of our Adonis.js API, and how to automate the process with CircleCI.

Happy coding!


Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.