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

Jest Advanced CLI Testing Example

Advanced testing patterns for CLI applications including mocking, fixtures, and integration tests.

Advanced Patterns

1. Async Command Testing

import { spawn } from 'child_process';

async function runCLIAsync(args: string[]): Promise<CLIResult> {
  return new Promise((resolve) => {
    const child = spawn(CLI_PATH, args, { stdio: 'pipe' });

    let stdout = '';
    let stderr = '';

    child.stdout?.on('data', (data) => {
      stdout += data.toString();
    });

    child.stderr?.on('data', (data) => {
      stderr += data.toString();
    });

    child.on('close', (code) => {
      resolve({ stdout, stderr, code: code || 0 });
    });
  });
}

test('should handle long-running command', async () => {
  const result = await runCLIAsync(['deploy', 'production']);
  expect(result.code).toBe(0);
}, 30000); // 30 second timeout

2. Environment Variable Mocking

describe('environment configuration', () => {
  const originalEnv = { ...process.env };

  afterEach(() => {
    process.env = { ...originalEnv };
  });

  test('should use API key from environment', () => {
    process.env.API_KEY = 'test_key_123';
    const { stdout, code } = runCLI('status');
    expect(code).toBe(0);
    expect(stdout).toContain('Authenticated');
  });

  test('should fail without API key', () => {
    delete process.env.API_KEY;
    const { stderr, code } = runCLI('status');
    expect(code).toBe(1);
    expect(stderr).toContain('API key not found');
  });
});

3. File System Fixtures

import fs from 'fs';
import os from 'os';

describe('config file handling', () => {
  let tempDir: string;

  beforeEach(() => {
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));
  });

  afterEach(() => {
    fs.rmSync(tempDir, { recursive: true, force: true });
  });

  test('should create config file', () => {
    const configFile = path.join(tempDir, '.config');
    const result = runCLI(`init --config ${configFile}`);

    expect(result.code).toBe(0);
    expect(fs.existsSync(configFile)).toBe(true);

    const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
    expect(config).toHaveProperty('api_key');
  });
});

4. Mocking External APIs

import nock from 'nock';

describe('API interaction', () => {
  beforeEach(() => {
    nock.cleanAll();
  });

  test('should fetch deployment status', () => {
    nock('https://api.example.com')
      .get('/deployments/123')
      .reply(200, { status: 'success', environment: 'production' });

    const { stdout, code } = runCLI('status --deployment 123');
    expect(code).toBe(0);
    expect(stdout).toContain('success');
    expect(stdout).toContain('production');
  });

  test('should handle API errors', () => {
    nock('https://api.example.com')
      .get('/deployments/123')
      .reply(500, { error: 'Internal Server Error' });

    const { stderr, code } = runCLI('status --deployment 123');
    expect(code).toBe(1);
    expect(stderr).toContain('API error');
  });
});

5. Test Fixtures

// test-fixtures.ts
export const createTestFixtures = () => {
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));

  // Create sample project structure
  fs.mkdirSync(path.join(tempDir, 'src'));
  fs.writeFileSync(
    path.join(tempDir, 'package.json'),
    JSON.stringify({ name: 'test-project', version: '1.0.0' })
  );

  return {
    tempDir,
    cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
  };
};

// Usage in tests
test('should build project', () => {
  const fixtures = createTestFixtures();

  try {
    const result = runCLI(`build --cwd ${fixtures.tempDir}`);
    expect(result.code).toBe(0);
    expect(fs.existsSync(path.join(fixtures.tempDir, 'dist'))).toBe(true);
  } finally {
    fixtures.cleanup();
  }
});

6. Snapshot Testing

test('help output matches snapshot', () => {
  const { stdout } = runCLI('--help');
  expect(stdout).toMatchSnapshot();
});

test('version format matches snapshot', () => {
  const { stdout } = runCLI('--version');
  expect(stdout).toMatchSnapshot();
});

7. Parameterized Tests

describe.each([
  ['development', 'dev.example.com'],
  ['staging', 'staging.example.com'],
  ['production', 'api.example.com'],
])('deploy to %s', (environment, expectedUrl) => {
  test(`should deploy to ${environment}`, () => {
    const { stdout, code } = runCLI(`deploy ${environment}`);
    expect(code).toBe(0);
    expect(stdout).toContain(expectedUrl);
  });
});

8. Interactive Command Testing

import { Readable, Writable } from 'stream';

test('should handle interactive prompts', (done) => {
  const child = spawn(CLI_PATH, ['init'], { stdio: 'pipe' });

  const inputs = ['my-project', 'John Doe', 'john@example.com'];
  let inputIndex = 0;

  child.stdout?.on('data', (data) => {
    const output = data.toString();
    if (output.includes('?') && inputIndex < inputs.length) {
      child.stdin?.write(inputs[inputIndex] + '\n');
      inputIndex++;
    }
  });

  child.on('close', (code) => {
    expect(code).toBe(0);
    done();
  });
});

9. Coverage-Driven Testing

// Ensure all CLI commands are tested
describe('CLI command coverage', () => {
  const commands = ['init', 'build', 'deploy', 'status', 'config'];

  commands.forEach((command) => {
    test(`${command} command exists`, () => {
      const { stdout } = runCLI('--help');
      expect(stdout).toContain(command);
    });

    test(`${command} has help text`, () => {
      const { stdout, code } = runCLI(`${command} --help`);
      expect(code).toBe(0);
      expect(stdout).toContain('Usage:');
    });
  });
});

10. Performance Testing

test('command executes within time limit', () => {
  const startTime = Date.now();
  const { code } = runCLI('status');
  const duration = Date.now() - startTime;

  expect(code).toBe(0);
  expect(duration).toBeLessThan(2000); // Should complete within 2 seconds
});

Best Practices

  1. Use Test Fixtures: Create reusable test data and cleanup functions
  2. Mock External Dependencies: Never make real API calls or database connections
  3. Test Edge Cases: Test boundary conditions, empty inputs, special characters
  4. Async Handling: Use proper async/await or promises for async operations
  5. Cleanup: Always cleanup temp files, reset mocks, restore environment
  6. Isolation: Tests should not depend on execution order
  7. Clear Error Messages: Write assertions with helpful failure messages

Common Advanced Patterns

  • Concurrent execution testing
  • File locking and race conditions
  • Signal handling (SIGTERM, SIGINT)
  • Large file processing
  • Streaming output
  • Progress indicators
  • Error recovery and retry logic

Resources