Notes Used to Craft this Lesson
Okay, so our project has a super simple greet Ace Command which we can use to quickly show how you can go about testing commands.
import { args, BaseCommand } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' export default class Greet extends BaseCommand { static commandName = 'greet' static description = 'Prints a greeting to the name provided' static options: CommandOptions = {} @args.string() declare name: string async run() { this.logger.log(`Hello ${this.name}!`) } }Copied!
- commands
- greet.ts
First, we'll make a new spec for this test. We'll put it within a commands folder, so that if you have multiple commands, you can group them.
node ace make:test commands/greet --suite=functionalCopied!
Commands can have a ton of purposes, but in a number of those, the purpose is to create side effects. Typically, you would want to run the command and assert that those side effects did what you expected. We'll see later in this series what testing those side effects looks like, via database assertions and the like.
Today, though, our focus is simply on: how do we make and run a command for testing purposes?
First, we'll import the ace service. We can use this to programmatically create an instance of a command. To do so, we'll pass the command class in directly as the first argument and an array of arguments in as the second. The create method will also validate our arguments.
import { test } from "@japa/runner"; import ace from "@adonisjs/core/services/ace"; import Greet from "../../../commands/greet.js"; test.group("Commands greet", () => { test("run the greet command when a valid argument is provided", async ({ assert, }) => { const command = await ace.create(Greet, ["Tom"]); }); });Copied!
Then, we'll execute the command to run it.
import { test } from "@japa/runner"; import ace from "@adonisjs/core/services/ace"; import Greet from "../../../commands/greet.js"; test.group("Commands greet", () => { test("run the greet command when a valid argument is provided", async ({ assert, }) => { const command = await ace.create(Greet, ["Tom"]); await command.exec(); }); });Copied!
Next, we'll make some basic assertions. Again, typically, you'd want to assert against the side effects the command is performing.
import { test } from "@japa/runner"; import ace from "@adonisjs/core/services/ace"; import Greet from "../../../commands/greet.js"; test.group("Commands greet", () => { test("run the greet command when a valid argument is provided", async ({ assert, }) => { const command = await ace.create(Greet, ["Tom"]); await command.exec(); assert.equal(command.name, "Tom"); command.assertSucceeded(); }); });Copied!
However, we can also assert against logs performed within the command as well. For this, we'll switch Ace to raw mode for each test and reset it after each test.
test.group("Commands greet", (group) => { group.each.setup(() => { ace.ui.switchMode("raw"); return () => ace.ui.switchMode("normal"); }); // ... });Copied!
Then, we can use the command.assertLog() method to assert one or more messages passed through our logger.
test.group("Commands greet", (group) => { group.each.setup(() => { ace.ui.switchMode("raw"); return () => ace.ui.switchMode("normal"); }); test("run the greet command when a valid argument is provided", async ({ assert, }) => { const command = await ace.create(Greet, ["Tom"]); await command.exec(); assert.equal(command.name, "Tom"); command.assertSucceeded(); command.assertLog("Hello Tom!"); }); });Copied!
Lastly, we can also ensure it fails when provided with invalid arguments.
test("fail to run the greet command when the argument is omitted", async ({ assert, }) => { assert.rejects( async () => ace.create(Greet, []), 'Missing required argument "name"' ); });Copied!
The name argument is required by the command, so by passing in no arguments, the create method should reject with an error stating such.
Let's quickly jump into this command and add a prompt. Prompts allow commands to ask questions in the console.
export default class Greet extends BaseCommand { static commandName = "greet"; static description = "Prints a greeting to the name provided"; static options: CommandOptions = {}; @args.string() declare name: string; async run() { this.logger.log(`Hello ${this.name}!`); await this.prompt.ask("What is your favorite color?", { validate(value) { return value.length > 2; }, }); } }Copied!
- commands
- greet.ts
When we save this, you'll notice our greet command test now stops and asks us a question in our console when that test is run. We don't want to have to answer that every time we run our tests, and it would also be good to assert it accepts values we expect.
So, within our test, let's trap this prompt so it no longer waits for our answer as our tests run. We can then use this trap to make assertions.
test("run the greet command when a valid argument is provided", async ({ assert, }) => { const command = await ace.create(Greet, ["Tom"]); command.prompt.trap("What is your favorite color?").assertPasses("Blue"); await command.exec(); assert.equal(command.name, "Tom"); command.assertSucceeded(); command.assertLog("Hello Tom!"); });Copied!
Perfect! Now, we have a validator on the prompt requiring our answer to be at least 3 characters, so let's add a quick test for that.
test("fail to accept a short prompt answer", async () => { const command = await ace.create(Greet, ["Phillip"]); command.prompt.trap("What is your favorite color?").assertFails("Mo"); await command.exec(); });Copied!
Great, Mo is not accepted and fails... sorry, Mo.