npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@push-based/node-cli-testing

v0.2.3

Published

### A e2e-testing library for node CLI and processes including sandbox generation on the fly

Downloads

87

Readme

node-cli-testing

A e2e-testing library for node CLI and processes including sandbox generation on the fly

npm

Motivation

Testing node code is easy as long as you can stick to unit tests (testing input and output of a single function). When it comes to more complex scenarios it get's pretty hard. Node processes, file outputs, integration tests, and console output is a hassle and involves many moving gears that are hard to setup an keep stable.

This is what this library tackles. A smooth out of the box experience for testing node CLIs and processes with unit, integration and e2e tests.

Features

  • 🚥 Testing node process output (stdout, stderr, exitCode)
  • 🧠 Handle .rc.jons files
  • ⌨️ Simulate keyboard interaction
  • 💬 Test console output
  • 🥸 Initializing a sandbox environment for each test
  • ⚙️ Automatically creating files needed for the test
  • 🧹 Cleanup after tests
  • 🦮 Helpers to check the generated files and folders of a node process

Install

You can install the node-cli-testing over npm or yarn as following:

npm install --save @push-based/node-cli-testing
# or
yarn add @push-based/node-cli-testing

Setup

The node-cli-testing lib can be imported as following:

import { CliProject } from '@push-based/node-cli-testing/cli-project';

let projectSandbox: CliProject;

const cfg: ProjectConfig = {
  root: './',
  bin: 'cli.js'
};

describe('The CLI configuration in default mode', () => {
  beforeEach(async () => {
    projectSandbox = await CliProjectFactory.create(cfg);
  });
  afterEach(async () => {
    projectSandbox = await CliProjectFactory.teardown(cfg);
  });

  it('should work', async () => {
    const { exitCode, stdout, stderr } = await projectSandbox.exec();
  });

});

Basic Usage of the CliProject wrapper

Setup a basic project and tests

The CliProject class makes it easy to execute a node file and handle it's in and outputs as well as process arguments. Let's set up a simple test for a CLI:

import { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';

let projectSandbox: CliProject;

const cfg: ProjectConfig = {
  root: './', // the directory in which the test should take place
  bin: 'cli.js' // the bin file the gets executed as node process
};

describe('The CLI', () => {
  beforeEach(async () => {
    projectSandbox = await CliProjectFactory.create(cfg);
  });
  afterEach(async () => {
    projectSandbox = await CliProjectFactory.teardown(cfg);
  });

  it('should work', async () => {
    const { exitCode, stdout, stderr } = await projectSandbox.exec();
    expect(stdin).toContain('some console output');
    expect(stderr).toBe('');
    expect(exitCode).toBe(0);
  });

});

Use setup helper

As is it kind of repetitive to set up beforeEach and afterEach this library provides a helper. This comes in handy when there are many different setups in one describe block.

The helper consumes a configuration for the project and handles setup and teardown internally.

import { ProjectConfig, withProject } from '@push-based/node-cli-testing/cli-project';

const cfg: ProjectConfig = {
  root: './',
  bin: 'cli.js' 
};
const cfg2: ProjectConfig = {
  root: './other/',
  bin: 'cli.js' 
};

describe('The CLI', () => {

  it('should work in version 1', withProject(cfg, async (prj) => {
    const { exitCode } = await prj.exec();
    expect(exitCode).toBe(0);
   })
  );
  
  it('should work in version 2', withProject(cfg2, async (prj) => {
    const { exitCode } = await prj.exec();
    expect(exitCode).toBe(0);
   })
  );

});

Use process arguments

Node processes can retrieve arguments over process.argv to be configurable. In the next snippet we pass arguments to a node process:

import { CliProject } from '@push-based/node-cli-testing/cli-project';

// Set up here.
// Details see the above example for setup a basic CLI project and test it

describe('The CLI', () => {
  beforeEach(/* same as in above */);
  afterEach(/* same as in above */);

  it('should work with params', async () => {
    // We can pass proces params as simple object ans it transporms it to standard process param style
    const { exitCode, stdout, stderr } = await projectSandbox.exec({verbose: true, count: 42, names: ['Srashti', 'Eliran', 'Mike']});
    
    expect(stdin).toContain('verbose mode is active');
    expect(stdin).toContain('count is 42');
    expect(stdin).toContain('names are Srashti, Eliran, Mike');
    
  });

});

The example above takes an object to define the process args like this:

{ verbose: false, count: 42, names: ['Srashti', 'Eliran', 'Mike'] }

Internally it converts them to the following string and passes it to the defined process:

--no-verbose --count=42 --name=Srashti --name=Eliran --name=Mike 

Test keyboard interaction

Often processes running in the console prompt to users and ask for some input. The lib exports some helper constants to interact simulate keyboard interaction.

import { CliProject, DOWN, SPACE, ENTER, DECLINE_BOOLEAN } from '@push-based/node-cli-testing/cli-project';

describe('The CLI', () => {
  beforeEach(/* ... */);
  afterEach(/* ... */);

  it('should work with params', async () => {
    const { stdout } = await projectSandbox.exec({prompt: true}, [DOWN, SPACE, ENTER, DECLINE_BOOLEAN]);
    
    expect(stdin).toContain('You selected the first option and hit enter');
    expect(stdin).toContain('You declined the option to generate a new file');
  });

});

Test file interaction

import { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';

let projectSandbox: CliProject;

const cfg: ProjectConfig = {
  root: './',
  bin: 'cli.js',
  create: {
    'fileA.txt': 'Content of file A'
  },
  delete: ['fileA.txt', 'fileB.txt']
};

describe('The CLI in a folder structure', () => {
  beforeEach(async () => {
    projectSandbox = await CliProjectFactory.create(cfg);
    // files from cfg.create are created here
    await projectSandbox.setup();
  });
  afterEach(async () => {
    // files from cfg.delete are deleted here    
    await projectSandbox.teardown();
  });

  it('should copy a file', async () => {
    const { exitCode, stdout, stderr } = await projectSandbox.exec({source: 'fileA.txt', dest: 'fileB.txt'});
    expect(stdout).toContain('file copied');
    
    const fileAContent = fs.readFileSync('fileA.txt', 'utf8').toString();
    const fileBContent = fs.readFileSync('fileB.txt', 'utf8').toString();
    
    expect(fileAContent).toBe(fileBContent);
  });

});

The CliProject will create all defined files from cfg.create when CliProject#setup is called. The CliProject will delete all defined filesNames from cfg.delete when CliProject#teardown is called.

Working with a .rc.json file

Often CLIs can be configured over a .rc.json file containing some default configuration. Let's create a test for this scenario:

import { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';

type MyRcJson = {
  count: number
};

let projectSandbox: CliProject<MyRcJson>;

const cfg: ProjectConfig<MyRcJson> = {
  root: './',
  bin: 'cli.js',
  rcFile: {
    '.rc.json': {
        count: 42
    }
  }
};

describe('The CLI configured over the .rc file', () => {
  beforeEach(async () => {
    projectSandbox = await CliProjectFactory.create(cfg);
    await projectSandbox.setup();
  });
  afterEach(async () => { 
    await projectSandbox.teardown();
  });

  it('should have count of 42', async () => {
    const { stdout } = await projectSandbox.exec();
    expect(stdout).toContain('count is 42');
  });

});

The CliProject will create the .rc.json file when CliProject#setup is called. The CliProject will delete the .rc.json file when CliProject#setup is called.

You can also create multiple rc files at once:

import { CliProject, CliProjectFactory } from '@push-based/node-cli-testing/cli-project';

const cfg: ProjectConfig = {
  root: './',
  bin: 'cli.js',
  rcFile: {
    '.rc.json': {
        count: 42
    }
  }
};

Advanced Usage

Creating custom CLI wrapper

Let's extend the CliProject class to add custom logic to handle a command:

import { CliProject, CliProjectFactory, TestResult } from '@push-based/node-cli-testing/cli-project';


type MyRcJson = {
  count: number
};

export class MyCliProject extends CliProject<MyRcJson> {
  
  constructor() {
    super();
  }

  $myCommand(processParams?: Partial<{count: number}>, userInput?: string[]): Promise<TestResult> {
    const prcParams: ProcessParams = { _: 'my-command', ...processParams } as ProcessParams;
    return this.exec(prcParams, userInput);
  }

}

Next, for better DX let's create a custom factory:

export class MyCliProjectFactory {
  static async create(cfg: ProjectConfig<MyRcJson>): Promise<MyCliProject<MyRcJson>> {
    const prj = new MyCliProject();
    await prj._setup(cfg);
    return prj;
  }
}

Now we can use it in our tests like this:

let projectSandbox: MyCliProject;

const cfg: ProjectConfig<MyRcJson> = {
  root: './',
  bin: 'cli.js'
};

describe('The CLI configuration in default mode', () => {
  beforeEach(async () => {
    projectSandbox = await MyCliProjectFactory.create(cfg);
  });

  it('should work', async () => {
    const { stdout } = await projectSandbox.$myCommand({count: 42});
    expect(stdout).toContain('count is 42');
  });

});

The above test would be equivalent to: node cli.js my-command --count=42