You are viewing the legacy version of AdonisJS. Visit for newer docs. This version will receive security patches until the end of 2021.

Getting Started

Manually testing your application by visiting each webpage or API endpoint can be tedious, and sometimes even impossible.

Automated testing is the preferred strategy to confirm your application continues to behave as expected as you make changes to your codebase.

In this guide, we learn about the benefits of testing and different ways to test your application’s code.

Test Cases

If you are new to testing, you may find it hard to understand the benefits.

Once you get into the habit of writing tests, your code quality and confidence about your code’s behavior should improve drastically.

Test Categories

Testing is divided into multiple categories, encouraging you to write different types of test cases with clear boundaries.

These test categories include:

Unit Tests

Unit tests are written to test small pieces of code in isolation.

For example, you might test a class directly without worrying how that class is used in the real world:

const { test } = use('Test/Suite')('Example unit test')
const UserValidator = use('App/Services/UserValidator')

test('validate user details', async ({ assert }) => {
  const validation = await UserValidator.validate({
    email: 'wrong email'

  assert.deepEqual(validation.messages(), [
      field: 'email',
      message: 'Invalid user email address'

Functional Tests

Functional tests are written to test your application like an end-user.

For example, you might programmatically open a browser and interact with various webpages to ensure they work as intended:

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

test('validate user details', async ({ browser }) => {
  const page = await browser.visit('/')

  await page
    .type('email', 'wrong email')

  page.session.assertError('email', 'Invalid user email address')

Both the above test examples validate the email address for a given user, but the approach is different based on the type of test you are writing.


As the Vow Provider is not installed by default, we need to pull it from npm:

> adonis install @adonisjs/vow

Next, register the provider in the start/app.js file aceProviders array:

const aceProviders = [
The provider is registered inside the aceProviders array since we do not want to boot the testing engine when running your app in production.

Installing @adonisjs/vow creates the following files and directory:


vowfiles.js is loaded before your tests are executed, and is used to define tasks that should occur before/after running all tests.


env.testing contains the environment variables used when running tests. This file gets merged with .env, so you only need to define values you want to override from the .env file.


All application tests are stored inside subfolders of the test directory. An example unit test is added to this directory when @adonisjs/vow is installed:

'use strict'

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

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

Running Tests

Installing the Vow Provider creates an example unit test for you, which can be executed by running the following command:

> adonis test
  ✓ make sure 2 + 2 is 4 (2ms)

total       : 1
passed      : 1
time        : 6ms

Testing Suite & Traits

Before we dive into writing tests, let’s understand some fundamentals which are important to understanding the flow of tests.


Each file is a test suite, defining a group of tests with similar behavior.

For example, we can have a suite of tests for user registration:

const Suite = use('Test/Suite')('User registration')

// or destructuring
const { test } = use('Test/Suite')('User registration')

The test function obtained from the Suite instance is used to define tests:

test('return error when credentials are wrong', async (ctx) => {
  // implementation


To avoid bloating the test runner with unnecessary functionality, AdonisJs ships different pieces of code as traits (the building blocks for your test suite).

For example, we call the Test/Browser trait so we can test via web browser:

const { test, trait } = use('Test/Suite')('User registration')


test('return error when credentials are wrong', async ({ browser }) => {
  const page = await browser.visit('/user')
In the example above, if we were to remove the Test/Browser trait, the browser object would be undefined inside our tests.

You can define custom traits with a closure or IoC container binding:

const { test, trait } = use('Test/Suite')('User registration')

trait(function (suite) {
  suite.Context.getter('foo', () => {
    return 'bar'

test('foo must be bar', async ({ foo, assert }) => {
  assert.equal(foo, 'bar')
Traits are helpful when you want to bundle a package to be used by others, though for most situations, you could simply use Lifecycle Hooks instead.


Each test has an isolated context.

By default, the context has only one property called assert which is an instance of chaijs/assert to run assertions.

You can pass custom values to each test context by defining getters or macros to be accessed inside the test callback closure (see the Traits closure example).

Lifecycle Hooks

Each suite has lifecycle hooks which can be used to perform repetitive tasks (for example, cleaning the database after each test):

const Suite = use('Test/Suite')('User registration')

const { before, beforeEach, after, afterEach } = Suite

before(async () => {
  // executed before all the tests for a given suite

beforeEach(async () => {
  // executed before each test inside a given suite

after(async () => {
  // executed after all the tests for a given suite

afterEach(async () => {
  // executed after each test inside a given suite


The assert object is an instance of chaijs/assert, passed to each test as a property of the test callback context.

To make your tests more reliable, you can also plan assertions to be executed for a given test. Let’s consider this example:

test('must throw exception', async ({ assert }) => {
  try {
    await badOperation()
  } catch ({ message }) {
    assert.equal(message, 'Some error message')

The above test passes even if an exception was never thrown and no assertions were run. This is a bad test, passing only because we structured it poorly.

To overcome this scenario, plan for your expected number of assertions:

test('must throw exception', async ({ assert }) => {

  try {
    await badOperation()
  } catch ({ message }) {
    assert.equal(message, 'Some error message')

In the above example, if badOperation doesn’t throw an exception, the test still fails since we planned for 1 assertion and 0 were made.