Files
gh-vanman2024-cli-builder-p…/skills/cli-testing-patterns/examples/jest-basic/README.md
2025-11-30 09:04:14 +08:00

3.6 KiB

Jest Basic CLI Testing Example

This example demonstrates basic CLI testing patterns using Jest for Node.js/TypeScript projects.

Setup

npm install --save-dev jest @types/jest ts-jest @types/node

Test Structure

import { execSync } from 'child_process';
import path from 'path';

describe('CLI Tool Tests', () => {
  const CLI_PATH = path.join(__dirname, '../bin/mycli');

  function runCLI(args: string) {
    try {
      const stdout = execSync(`${CLI_PATH} ${args}`, {
        encoding: 'utf8',
        stdio: 'pipe',
      });
      return { stdout, stderr: '', code: 0 };
    } catch (error: any) {
      return {
        stdout: error.stdout || '',
        stderr: error.stderr || '',
        code: error.status || 1,
      };
    }
  }

  test('should display version', () => {
    const { stdout, code } = runCLI('--version');
    expect(code).toBe(0);
    expect(stdout).toContain('1.0.0');
  });

  test('should display help', () => {
    const { stdout, code } = runCLI('--help');
    expect(code).toBe(0);
    expect(stdout).toContain('Usage:');
  });

  test('should handle unknown command', () => {
    const { stderr, code } = runCLI('unknown-command');
    expect(code).toBe(1);
    expect(stderr).toContain('unknown command');
  });
});

Running Tests

# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Run in watch mode
npm run test:watch

Key Patterns

1. Command Execution Helper

Create a reusable runCLI() function that:

  • Executes CLI commands using execSync
  • Captures stdout, stderr, and exit codes
  • Handles both success and failure cases

2. Exit Code Testing

Always test exit codes:

  • 0 for success
  • Non-zero for errors
  • Specific codes for different error types

3. Output Validation

Test output content using Jest matchers:

  • .toContain() for substring matching
  • .toMatch() for regex patterns
  • .toBe() for exact matches

4. Error Handling

Test error scenarios:

  • Unknown commands
  • Invalid options
  • Missing required arguments
  • Invalid argument types

Example Test Cases

describe('deploy command', () => {
  test('should deploy with valid arguments', () => {
    const { stdout, code } = runCLI('deploy production --force');
    expect(code).toBe(0);
    expect(stdout).toContain('Deploying to production');
  });

  test('should fail without required arguments', () => {
    const { stderr, code } = runCLI('deploy');
    expect(code).toBe(1);
    expect(stderr).toContain('missing required argument');
  });

  test('should validate environment names', () => {
    const { stderr, code } = runCLI('deploy invalid-env');
    expect(code).toBe(1);
    expect(stderr).toContain('invalid environment');
  });
});

Best Practices

  1. Isolate Tests: Each test should be independent
  2. Use Descriptive Names: Test names should describe what they validate
  3. Test Both Success and Failure: Cover happy path and error cases
  4. Mock External Dependencies: Don't make real API calls or file system changes
  5. Use Type Safety: Leverage TypeScript for better test reliability
  6. Keep Tests Fast: Fast tests encourage frequent running

Common Pitfalls

  • Not testing exit codes
  • Only testing success cases
  • Hardcoding paths instead of using path.join()
  • Not handling async operations properly
  • Testing implementation details instead of behavior

Resources