Command-line tests

Command-line tests

Command-line tests refer to testing custom Ace commands that are part of your application or the package codebase.

In this guide, we will learn how to write tests for a command, mock the logger output, and trap CLI prompts.

Basic example

Let's start by creating a new command named greet.

node ace make:command greet
import { BaseCommand } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
export default class Greet extends BaseCommand {
static commandName = 'greet'
static description = 'Greet a username by name'
static options: CommandOptions = {}
async run() {
this.logger.info('Hello world from "Greet"')
}
}

Let's create a unit test inside the tests/unit directory. Feel free to define the unit test suite if it is not already defined.

node ace make:test commands/greet --suite=unit
# DONE: create tests/unit/commands/greet.spec.ts

Let's open the newly created file and write the following test. We will use the ace service to create an instance of the Greet command and assert that it exits successfully.

import { test } from '@japa/runner'
import Greet from '#commands/greet'
import ace from '@adonisjs/core/services/ace'
test.group('Commands greet', () => {
test('should greet the user and finish with exit code 1', async () => {
/**
* Create an instance of the Greet command class
*/
const command = await ace.create(Greet, [])
/**
* Execute command
*/
await command.exec()
/**
* Assert command exited with status code 0
*/
command.assertSucceeded()
})
})

Let's run the test using the following ace command.

node ace test --files=commands/greet

Testing logger output

The Greet command currently writes the log message to the terminal. To capture this message and write an assertion for it, we will have to switch the UI library of ace into raw mode.

In raw mode, the ace will not write any logs to the terminal. Instead, keep them within memory for writing assertions.

We will use the Japa each.setup hook to switch into and out of the raw mode.

test.group('Commands greet', (group) => {
group.each.setup(() => {
ace.ui.switchMode('raw')
return () => ace.ui.switchMode('normal')
})
// test goes here
})

Once the hook has been defined, you can update your test as follows.

test('should greet the user and finish with exit code 1', async () => {
/**
* Create an instance of the Greet command class
*/
const command = await ace.create(Greet, [])
/**
* Execute command
*/
await command.exec()
/**
* Assert command exited with status code 0
*/
command.assertSucceeded()
/**
* Assert the command printed the following log message
*/
command.assertLog('[ blue(info) ] Hello world from "Greet"')
})

Testing tables output

Similar to testing the log messages, you can write assertions for the table output by switching the UI library into raw mode.

async run() {
const table = this.ui.table()
table.head(['Name', 'Email'])
table.row(['Harminder Virk', 'virk@adonisjs.com'])
table.row(['Romain Lanz', 'romain@adonisjs.com'])
table.row(['Julien-R44', 'julien@adonisjs.com'])
table.row(['Michaël Zasso', 'targos@adonisjs.com'])
table.render()
}

Given the above table, you can write an assertion for it as follows.

const command = await ace.create(Greet, [])
await command.exec()
command.assertTableRows([
['Harminder Virk', 'virk@adonisjs.com'],
['Romain Lanz', 'romain@adonisjs.com'],
['Julien-R44', 'julien@adonisjs.com'],
['Michaël Zasso', 'targos@adonisjs.com'],
])

Trapping prompts

Since prompts blocks the terminal waiting for manual input, you must trap and respond to them programmatically when writing tests.

Prompts are trapped using the prompt.trap method. The method accepts the prompt title (case sensitive) and offers a chainable API for configuring additional behavior.

The traps are removed automatically after the prompt gets triggered. An error will be thrown if the test finishes without triggering the prompt with a trap.

In the following example, we place a trap on a prompt titled "What is your name?" and answer it using the replyWith method.

const command = await ace.create(Greet, [])
command.prompt
.trap('What is your name?')
.replyWith('Virk')
await command.exec()
command.assertSucceeded()

Choosing options

You can choose options with a select or a multi-select prompt using the chooseOption and chooseOptions methods.

command.prompt
.trap('Select package manager')
.chooseOption(0)
command.prompt
.trap('Select database manager')
.chooseOptions([1, 2])

Accepting or rejecting confirmation prompts

You can accept or reject prompts displayed using the toggle and the confirm methods.

command.prompt
.trap('Want to delete all files?')
.accept()
command.prompt
.trap('Want to delete all files?')
.reject()

Asserting against validations

To test the validation behavior of a prompt, you can use the assertPasses and assertFails methods. These methods accept the value of the prompt and test it against the prompt's validate method.

command.prompt
.trap('What is your name?')
// assert the prompt fails when an empty value is provided
.assertFails('', 'Please enter your name')
command.prompt
.trap('What is your name?')
.assertPasses('Virk')

Following is an example of using assertions and replying to a prompt together.

command.prompt
.trap('What is your name?')
.assertFails('', 'Please enter your name')
.assertPasses('Virk')
.replyWith('Romain')

Available assertions

Following is the list of assertion methods available on a command instance.

assertSucceeded

Assert the command exited with exitCode=0.

await command.exec()
command.assertSucceeded()

assertFailed

Assert the command exited with non-zero exitCode.

await command.exec()
command.assertSucceeded()

assertExitCode

Assert the command exited with a specific exitCode.

await command.exec()
command.assertExitCode(2)

assertNotExitCode

Assert the command exited with any exitCode, but not the given exit code.

await command.exec()
command.assertNotExitCode(0)

assertLog

Assert the command writes a log message using the this.logger property. You can optionally assert the output stream as stdout or stderr.

await command.exec()
command.assertLog('Hello world from "Greet"')
command.assertLog('Hello world from "Greet"', 'stdout')

assertLogMatches

Assert the command writes a log message that matches the given regular expression.

await command.exec()
command.assertLogMatches(/Hello world/)

assertTableRows

Assert the command prints a table to the stdout. You can provide the table rows as an array of columns. The columns are represented as an array of cells.

await command.exec()
command.assertTableRows([
['Harminder Virk', 'virk@adonisjs.com'],
['Romain Lanz', 'romain@adonisjs.com'],
['Julien-R44', 'julien@adonisjs.com'],
])