Testing
Commands are plain objects, so you can test them without spawning a process. testCommand() runs one
against a throwaway CLI and hands back both what it returned and what it printed.
testCommand()
Section titled “testCommand()”import { command, testCommand } from 'clibuilder'
test('cmd-a returns x and says miku', async () => { const { result, messages } = await testCommand( command({ name: 'cmd-a', description: 'a command', run() { this.ui.info('miku') return 'x' } }), 'cmd-a' )
expect(result).toBe('x') expect(messages).toBe('miku')})resultis whateverrun()returned or resolved to.exitCodeis the code the cli would have exited with, orundefinedwhen the command did not fail — see Failing.messagesis everything the command wrote throughthis.ui, joined with newlines —info,warn, anderrorall land here, so a test can assert on user-facing output without capturing stdout.
The second parameter is the command line after the CLI name. Pass arguments and options the way a user would type them:
await testCommand(sumCommand, 'sum --verbose 1 2 3')Testing with config
Section titled “Testing with config”The third parameter supplies the config the command would have loaded from disk, so no fixture file is needed:
const { result } = await testCommand( command({ name: 'cfg', description: 'reads config', config: z.object({ a: z.string() }), run() { return this.config } }), 'cfg', { a: 'hi' })
expect(result).toEqual({ a: 'hi' })The config you pass still goes through the command’s schema, so this is also how you test that an invalid config is rejected.
Testing sub-commands
Section titled “Testing sub-commands”Address a nested command by its full path, exactly as a user would:
const repo = command({ name: 'repo', description: 'manage repositories', commands: [create, remove]})
await testCommand(repo, 'repo create my-project')Injecting dependencies with context
Section titled “Injecting dependencies with context”A command’s context is the seam for the I/O it does. Declare the real implementation on the
command, and give the test a fake — no module mocking involved.
const show = command({ name: 'show', description: 'print a file', context: { readFile }, arguments: [{ name: 'file', description: 'file to print' }], async run(args) { this.ui.info(await this.context.readFile(args.file, 'utf8')) }})
test('show prints the file', async () => { const { messages } = await testCommand( { ...show, context: { readFile: async () => 'contents' } }, 'show a.txt' ) expect(messages).toBe('contents')})Testing the whole CLI
Section titled “Testing the whole CLI”For an end-to-end check, parse() an argv array yourself — it resolves to the invoked command’s
return value:
const app = cli({ name: 'app', version: '1.0.0' }).default({ run: () => 42 })
expect(await app.parse(['node', 'app'])).toBe(42)Note that parse() takes a full process.argv: the first two entries are skipped as the node
binary and the script path.