Limitless

Limitless

CGPA Calculator with AdonisJS: API Testing

CGPA Calculator with AdonisJS: API Testing

Subscribe to my newsletter and never miss my upcoming articles

Testing is an important part of any software development project. Testing gives us confidence in our code and helps us catch bugs before deployment. Welcome to part 5 of this series. We wrote the final APIs in part 4. Now, we will write functional tests for our APIs. If you will like to skip the previous steps, clone the repo and checkout to the more-relationships-and-seeding branch, then code along.

Functional Testing

According to Wikipedia

Functional testing is a quality assurance (QA) process and a type of black-box testing that bases its test cases on the specifications of the software component under test.

Basically, functional tests are written in a way that matches how a real user will interact with the app. Take, for example, we want to test course addition from scratch. We will

  1. Open a headless or real browser
  2. Navigate to the register page
  3. Register the user
  4. Navigate to the course addition form
  5. Fill in the details
  6. Sumit the form We will have a test that will carry out these steps and ensure no error is thrown. If an error is thrown, then our tests fail and we will have to investigate what went wrong.

Getting started testing Adonis apps

@adonisjs/vow, the standard library built for the framework uses chai under the hood for assertions. We will mainly be testing using assertions. Get started by installing vow

adonis install @adonisjs/vow

The installation adds three files to your project. Add the configuration to the aceProviders array of app.js

const aceProviders = [
  // ...other providers
  '@adonisjs/vow/providers/VowProvider'
]

You can see how testing works by testing example.spec.js

adonis test

Output

  Example
    βœ“ make sure 2 + 2 is 4 (2ms)

   PASSED 

  total       : 1
  passed      : 1
  time        : 6ms

Pre-testing checklist: Suites and Traits

Below is the content of the example test file.

'use strict'

const { test } = use('Test/Suite')('Example')

test('make sure 2 + 2 is 4', async ({ assert }) => {
  assert.equal(2 + 2, 4)
})

Notice that we are destructuring the test function from Test/Suite. Since we are testing APIs, we need a JS version of Postman. This is provided by Test/ApiClient, a trait. Traits were implemented to keep the test runner lean, so any desired functionality is required when needed.

Basically, we obtain trait from Suite and require the Test/ApiClient trait. Since some of our routes require authentication, we also require the Auth/Client trait.

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

trait("Test/ApiClient");
trait("Auth/Client");

To understand more about Suites and Traits, I suggest you read the docs. The Adonis team did a job explaining Suites and Traits.

Our first tests

We will structure our tests such that each controller will contain tests for each method. Go ahead and delete example.spec.js, then run this

adonis make:test User -f
# output: create: test/functional/user.spec.js

Replace the content of user.spec.js with this

"use strict";

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

trait("Test/ApiClient");
trait("Auth/Client");

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

Testing registration

We'll follow the convention of using the present tense on test cases. Here, we are testing the register route and asserting that the status is 201.

test("registers a new user", async ({ client }) => {
  const response = await client
    .post(`/api/v1/register`)
    .send({
      email: "test-user@email.com",
      password: "some password",
      grade_system: "5",
    })
    .end();

  await response.assertStatus(201);
});

Kent C. Doods always says to ensure your test is working, feed it a wrong assertion. So we'll asset a 200 and run our tests.

- response.assertStatus(201);
+ response.assertStatus(200);

Now, run the tests

Failing tests

Our tests said expected 201 to equal 200. We know that it is meant to be 201, so it means our test is working. Now return the assertion to its previous state and run the tests again.

Screenshot from 2020-11-28 19-49-57.png

Huh 🀨? 400? Remember that the register() method in UserController.js returns errors for non-unique emails. We should probably write a test for that too eh? Change the email and run the test again.

Passing registration test

Hurray 🎊! It worked! That felt manual and isn't ideal. You know what will be better? A separate testing database which will be migrated and seeded before any tests run and revert the migrations after all the tests have run.

Configuring the testing setup

First things first, let's create a testing DB. If you are using sqlite, create one in the database directory.

touch database/testing.sqlite

If you are using a different DB, create a testing database. Name it whatever you wish.

In .env.testing, add your database name

DB_DATABASE=testing

.env.testing is used to override the default values of .env when testing. We will complete our configuration in vowfile.js. vowfile.js is used for pre-tests and post-tests setup. First of all, uncomment the ace import: // const ace = require('@adonisjs/ace'). In the runner.before function, uncomment // await ace.call('migration:run', {}, { silent: true }) and add this below it

    await ace.call('seed', {}, { silent: true })

Likewise, in runner.after, uncomment // await ace.call('migration:reset', {}, { silent: true }).

Now, run your tests multiple times to verify that we don't run into that 400 again.

Testing an auth only route:

Let's test a route that requires authentication: update/profile. First, we will create the case alongside a test user.

test("updates a user's profile", async ({ client }) => {
  const user = await User.create({
    email: "some-other-email@email.com",
    password: "some password",
  });
});

Then we call the API with a loginVia method attached. Note that we won't be able to use loginVia without requiring trait("Auth/Client"). Finally, we assert the status to be 200 and the returned JSON to contain the names.

  response.assertStatus(200);

  response.assertJSONSubset({
    firstName: "John",
    lastName: "Doe",
  });

We could have also used assertJSON, but it will require that we include every field of the returned JSON. This may not be ideal for every case. Learn more about assertions here.

Test the new test case.

Two passing test cases

So, what now?

Now, you test the other routes. Write as many test cases as you deem fit. If you compare your tests with mine, checkout the main branch on this repo

Farewell

It's been 5 articles of bravery and honour. It can't deny how impressed I am to see you become an Adonis hero my young warlock. No that's not right, we've been writing APIs πŸ˜‚. But it's been quite a journey. I hope you enjoyed every bit of it. Please I need your honest feedback on

  1. The tutorial style
  2. If I explained too much
  3. The minimal use of images and memes. Feel free to add a comment. I really want to grow as a writer and your feedback matters a lot. Thank you for following along. Adios ✌🏾🧑.
Β 
Share this