Testing
Testing CLI commands is essential for ensuring your commands work correctly. Navios Commander provides programmatic command execution, making it easy to test commands in isolation.
Basic Testing
Use executeCommand() to test commands programmatically:
import { CommanderFactory } from '@navios/commander'
import { AppModule } from './app.module'
describe('GreetCommand', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should greet with default greeting', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('greet', { name: 'World' })
expect(consoleSpy).toHaveBeenCalledWith('Hello, World!')
consoleSpy.mockRestore()
})
it('should greet with custom greeting', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('greet', { name: 'World', greeting: 'Hi' })
expect(consoleSpy).toHaveBeenCalledWith('Hi, World!')
consoleSpy.mockRestore()
})
})
Testing with Mocked Services
Mock services to test commands in isolation:
import { CommanderFactory } from '@navios/commander'
import { AppModule } from './app.module'
import { UserService } from './user.service'
describe('ShowUserCommand', () => {
let app: CommanderApplication
let mockUserService: jest.Mocked<UserService>
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
// Get the container and replace service
const container = app.getContainer()
mockUserService = {
getUser: jest.fn().mockResolvedValue({
id: '123',
name: 'John Doe',
email: '[email protected]',
}),
} as any
// Replace service in container
// Note: This requires access to container internals
// In practice, you might use dependency injection tokens
})
afterAll(async () => {
await app.close()
})
it('should show user information', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:show', { userId: '123' })
expect(mockUserService.getUser).toHaveBeenCalledWith('123')
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('John Doe')
)
consoleSpy.mockRestore()
})
})
Testing Validation
Test that commands validate options correctly:
describe('CreateUserCommand', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should reject invalid email', async () => {
await expect(
app.executeCommand('user:create', {
name: 'John',
email: 'invalid-email',
})
).rejects.toThrow()
})
it('should reject missing required fields', async () => {
await expect(
app.executeCommand('user:create', {
name: 'John',
// email is missing
})
).rejects.toThrow()
})
it('should accept valid options', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:create', {
name: 'John Doe',
email: '[email protected]',
})
expect(consoleSpy).toHaveBeenCalled()
consoleSpy.mockRestore()
})
})
Testing Error Handling
Test that commands handle errors correctly:
describe('DeleteUserCommand', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should throw error when user not found', async () => {
await expect(
app.executeCommand('user:delete', { userId: 'nonexistent' })
).rejects.toThrow('User not found')
})
it('should delete user successfully', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:delete', { userId: '123' })
expect(consoleSpy).toHaveBeenCalledWith('User deleted')
consoleSpy.mockRestore()
})
})
Testing with Test Containers
Create test-specific modules for isolated testing:
import { CliModule } from '@navios/commander'
import { TestUserService } from './test-user.service'
import { ShowUserCommand } from './show-user.command'
@CliModule({
commands: [ShowUserCommand],
})
export class TestModule {}
describe('ShowUserCommand', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(TestModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should show user', async () => {
// Test with test module
await app.executeCommand('user:show', { userId: '123' })
})
})
Testing Command Output
Test command output and side effects:
describe('ListUsersCommand', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should list users in JSON format', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:list', { json: true })
const output = consoleSpy.mock.calls[0][0]
const users = JSON.parse(output)
expect(Array.isArray(users)).toBe(true)
expect(users.length).toBeGreaterThan(0)
consoleSpy.mockRestore()
})
it('should list users in human-readable format', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:list', { json: false })
const output = consoleSpy.mock.calls[0][0]
expect(output).toContain('Users:')
consoleSpy.mockRestore()
})
})
Integration Testing
Test commands with real services:
describe('UserCommands Integration', () => {
let app: CommanderApplication
let testDb: TestDatabase
beforeAll(async () => {
testDb = await setupTestDatabase()
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await testDb.cleanup()
await app.close()
})
it('should create and show user', async () => {
// Create user
await app.executeCommand('user:create', {
name: 'Test User',
email: '[email protected]',
})
// Show user
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('user:show', { userId: 'test-id' })
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Test User')
)
consoleSpy.mockRestore()
})
})
Testing Execution Context
Test commands that use execution context:
describe('CommandLogger', () => {
let app: CommanderApplication
beforeAll(async () => {
app = await CommanderFactory.create(AppModule)
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should log command execution', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
await app.executeCommand('process', { file: 'test.txt' })
// Check that execution context was used
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Command: process')
)
consoleSpy.mockRestore()
})
})
Testing Module Loading
Test that modules load correctly:
describe('Module Loading', () => {
it('should load all commands from module', async () => {
const app = await CommanderFactory.create(AppModule)
await app.init()
const commands = app.getAllCommands()
expect(commands).toHaveLength(3)
expect(commands.map(c => c.path)).toContain('user:create')
expect(commands.map(c => c.path)).toContain('user:delete')
expect(commands.map(c => c.path)).toContain('user:list')
await app.close()
})
it('should load commands from imported modules', async () => {
const app = await CommanderFactory.create(AppModule)
await app.init()
const commands = app.getAllCommands()
// Commands from imported modules should be available
expect(commands.map(c => c.path)).toContain('db:migrate')
expect(commands.map(c => c.path)).toContain('db:seed')
await app.close()
})
})
Best Practices
1. Isolate Tests
Each test should be independent:
// ✅ Good: Isolated test
it('should create user', async () => {
await app.executeCommand('user:create', { name: 'John', email: '[email protected]' })
// Test assertions
})
// ❌ Avoid: Tests that depend on each other
it('should create user', async () => {
await app.executeCommand('user:create', { name: 'John', email: '[email protected]' })
})
it('should show user', async () => {
// Depends on previous test
await app.executeCommand('user:show', { userId: '123' })
})
2. Mock External Dependencies
Mock services that interact with external systems:
// ✅ Good: Mocked external service
const mockEmailService = {
send: jest.fn().mockResolvedValue(true),
}
// ❌ Avoid: Real external service calls
// This would send actual emails during tests
3. Test Error Cases
Test both success and error scenarios:
// ✅ Good: Test both cases
it('should handle valid input', async () => {
// Success case
})
it('should reject invalid input', async () => {
// Error case
})
4. Clean Up Resources
Always clean up resources after tests:
// ✅ Good: Clean up
afterAll(async () => {
await app.close()
await testDb.cleanup()
})
Next Steps
- Learn about commands in detail
- Explore programmatic execution for testing
- Check out best practices for command design