Introduction

AdonisJS has out of the box support for testing, and there is no need to install any third-party packages for the same. Just run the node ace test and the magic will happen.

If you are running an older version that dos not have testing support, ensure to follow our activation guide .

Every fresh installation of AdonisJS ships with an example functional test written within the tests/functional/hello-world.spec.ts file. Let's open this file and learn how tests are written in AdonisJS.

AdonisJS uses Japa (A homegrown testing framework) for writing and executing tests. Therefore, we highly recommend you to read the Japa docs once.

import { test } from '@japa/runner'
test('display welcome page', async ({ client }) => {
const response = await client.get('/')
response.assertStatus(200)
response.assertTextIncludes('<h1 class="title"> It Works! </h1>')
})
  • A test is registered using the test function exported by the @japa/runner package.
  • The test function accepts the title as the first argument and the implementation callback as the second argument.
  • The implementation callback receives the Test context . Test context contains additional properties you can use to have a better testing experience.

Let's run the test by executing the following command.

node ace test
# [ info ] running tests...
# tests/functional/hello-world.spec.ts
# ✔ display welcome page (24ms)
# PASSED
# total : 1
# passed : 1
# duration : 28ms

Now let's re-run the test command, but this time with the --watch flag. The watcher will watch for file system changes and executes the tests after every file change.

node ace test --watch

Test suites

AdonisJS organizes tests into multiple suites. Tests for every suite live within its sub-directory. For example:

  • Functional tests are stored inside tests/functional/ directory.
  • Unit tests are stored inside the tests/unit/ directory.

Suites are registered inside the .adonisrc.json file, and you can remove/add suites as per requirements. A suite combines a unique name and a glob pattern for the files.

You can also use the make:suite command to create a new test suite and register it inside the .adonisrc.json file.

.adonisrc.json
{
"tests": {
"suites": [
{
"name": "functional",
"files": "tests/functional/**/*.spec(.ts|.js)"
}
]
}
}

You can also register lifecycle hooks for every test suite. The hooks are registered within the tests/bootstrap.ts file using the configureSuite method.

In the following example, AdonisJS registers a setup hook to start the HTTP server for the functional test suite.

export const configureSuite: Config['configureSuite'] = (suite) => {
if (suite.name === 'functional') {
suite.setup(() => TestUtils.httpServer().start())
}
}

Configuring the tests runner

AdonisJS configures the test runner within the test.ts file inside the root of your project. This file first boots the AdonisJS application and then runs tests using Japa.

You will never touch the test.ts file for the most part. Instead, we recommend you to use the tests/bootstrap.ts file to configure the tests runner further or run custom logic before/after the tests.

The bootstrap file exports the following properties, which are then given to Japa.

tests/bootstrap.ts
export const plugins: Config['plugins'] = []
export const reporters: Config['reporters'] = []
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [],
teardown: [],
}
export const configureSuite: Config['configureSuite'] = (suite) => {
}

plugins

The plugins property accepts an array of Japa plugins. By default, we register the following plugins.

  • assert - Assert module to make assertions.
  • runFailedTests - A plugin to run only failed tests (if any).
  • apiClient - An API client for testing HTTP endpoints.

reporters

The reporters property accepts an array of Japa reporters. We register the spec-reporter to display the progress of tests on the terminal.


runnerHooks

You can use the runnerHooks property to run actions before or after the tests (across all the suites).

  • The setup hooks are executed before all the tests.
  • The teardown hooks are executed after all the tests.

configureSuite

The configureSuite method is executed with an instance of the Japa suite class. You can use the suite instance to configure it.

Environment variables

During tests, AdonisJS automatically sets the value of NODE_ENV to test.

We also load the .env.test file and merge the values defined inside this file with existing environment variables. The following overrides are defined by default.

NODE_ENV=test
ASSETS_DRIVER=fake
SESSION_DRIVER=memory
  • ASSETS_DRIVER property switches the driver for serving bundled assets to a fake implementation. Doing so allows you to run tests without compiling the frontend assets using Webpack.
  • SESSION_DRIVER is switched to persist session data within memory and access it during tests. Using any other driver will break the tests.

Creating tests

You can create tests using the node ace make:test command. The command accepts the suite name as the first argument, followed by the test file name.

node ace make:test functional list_users
# CREATE: tests/functional/list_users.spec.ts

You can also create a nested file structure as follows.

node ace make:test functional users/list
# CREATE: tests/functional/users/list.spec.ts

Running tests

You can run tests by executing the node ace test command. Also, you can run tests for a specific suite by passing the suite name.

# Runs all tests
node ace test
# Only functional tests are executed
node ace test functional
# unit and functional tests are executed sequentially
node ace test unit functional
# Only tests with an "orders" or "upload" tag in the "unit" and "functional" suites
node ace test --tags="orders,upload" unit functional

The test command accepts the following flags.

  • --watch: Run tests in the watch mode. The watcher will run only tests from the modified file if a test file is changed. Otherwise, all tests are executed.
  • --tags: Run tests that have one or more of the mentioned tags.
  • --ignore-tags: The inverse of the --tags flag. Only run tests that do not have all of the mentioned tags.
  • --files: Cherry pick and run tests from mentioned files.
  • --timeout: Define the global timeout for all the tests.
  • --force-exit: Force exit the test process if it does not ends gracefully.
  • --tests: Run specific tests by title.

Database management

This section covers database migrations, running seeders, and using global transactions to have a clean database state between tests.

Make sure you have @adonisjs/lucid installed for the following examples to work.

Migrating database

You can migrate the database before running all the tests and roll it back after the tests. This can be done by registering the TestUtils.db().migrate() hook within the tests/bootstrap.ts file.

tests/bootstrap.ts
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [
() => TestUtils.ace().loadCommands(),
() => TestUtils.db().migrate()
],
teardown: [],
}

Seeding database

You can also run database seeders by calling the TestUtils.db().seed() method.

setup: [
() => TestUtils.ace().loadCommands(),
() => TestUtils.db().migrate()
() => TestUtils.db().seed()
],

Global transactions

We recommend you to use the Database global transactions to have a clean database state in-between tests.

In the following example, we start a global transaction before all the tests and roll back it after the tests.

The group.each.setup method runs before every test inside the group.

import Database from '@ioc:Adonis/Lucid/Database'
test.group('Group name', (group) => {
group.each.setup(async () => {
await Database.beginGlobalTransaction()
return () => Database.rollbackGlobalTransaction()
})
})

If you are using multiple database connections, then you can define a hook for each connection. For example:

group.each.setup(async () => {
await Database.beginGlobalTransaction('pg')
return () => Database.rollbackGlobalTransaction('pg')
})
group.each.setup(async () => {
await Database.beginGlobalTransaction('mysql')
return () => Database.rollbackGlobalTransaction('mysql')
})