promise-puppeteer

Easy to use JS promise test double/fake/mock and execution analysis library

Usage no npm install needed!

<script type="module">
  import promisePuppeteer from 'https://cdn.skypack.dev/promise-puppeteer';
</script>

README

Promise Puppeteer

Table Of Contents

Introduction

Promise Puppeteer is a drop-in test solution for handling ES-Next and Promise/A+ promises in your code under test. The goal of Promise Puppeteer is to make it as smooth and easy as possible to test code consuming promises without having to do a bunch of test gymnastics.

Why use Promise Puppeteer:

  • Easy to use -- API is exposed to developer to allow for full control over promise execution
  • Predictable -- Executes promise code as it behaves in the wild, making it easier to verify everything works as expected
  • Deterministic execution -- Your test is separated from the world: resolve or reject from within tests
  • Clear communication -- Built to fail when code acts unpredictably: Resolve and reject throw when called twice; Optionally throws an error if no catch or onFailure behavior is registered (default: on)
  • Easy execution analysis -- Optional step-by-step execution of resolve behavior for easier analysis

Compatibility:

  • ES-Next Promise Standard:
    • Promise
      • all
      • race
      • new Promise()
    • Thenable
      • then
      • catch
      • finally
  • Promise/A+
    • then supports both onSuccess and onFailure optionally

Promise Puppeteer works both in Node and client-side code, so it integrates seamlessly into all of your test scenarios. With its clear, simple API, it is easy to either execute through the entire code stack or perform step-by-step evaluation of promise resolution.

Setup

Setup is as simple as performing an NPM installation and including the library in your test environment:

npm install promise-puppeteer --save-dev

The library exists at the following path:

project-root/node_modules/promise-puppeteer/index.js

That's it!

Test Examples

A common test example

A common test setup scenario would look like the following (using the Mocha test framework):

const sinon = require('sinon');

const promiseDoubleFactory = require('promise-puppeteer');
const moduleUnderTestFactory = require('./moduleUnderTestFactory');

describe('Module Under Test', function () {

    let thenableFake;
    let moduleUnderTest;

    beforeEach(function () {
        thenableFake = promiseDoubleFactory.getThenableFake();

        const MyService = require('./MyService');
        sinon.stub(MyService, 'doAsyncThing').callsFake(() => thenableFake);

        moduleUnderTest = moduleUnderTestFactory(MyService);
    });

    it('consumes a promise', function() {
        // Call method under test
        moduleUnderTest.doSomethingAsync();

        // Initiate promise resolution, firing all thens, catches and finallys
        thenableFake.resolve({ foo: 'bar' });

        // perform test assertion around result from doSomethingAsync
    });

});

A running thenable fake

An example of consuming a Promise Puppeteer thenable straight from the actual test suite is as follows:

const thenableFake = getThenableFake();

const thenStub = sinon.stub();

thenableFake
    .then(value => value + 1)
    .then(value => value * 2)
    .then(thenStub)
    .catch(() => null)
    .resolve(5);

assert.equal(thenStub.args[0][0], 12);

It's worth noting thenableFake adheres to the ES-Next promise standard, with the addition of resolve, which will execute the promise as if it were resolved from an async executor (action).

Step-wise then resolution

You can step through a promise resolution with the resolveNext function. The following execution allows the user to analyze what happens as each then executes and exits.

const thenableFake = getThenableFake();

const thenStub = sinon.stub();

thenableFake
    .then(value => value + 1)
    .then(value => value * 2)
    .then(thenStub)
    .catch(() => null);

const result1 = thenableFake.resolveNext(5); // runs the first 'then'
const result2 = thenableFake.resolveNext(result1); // runs the second 'then'
const result3 = thenableFake.resolveNext(result2); // runs the third 'then'

assert.equal(thenStub.args[0][0], result3);
assert.equal(result3, 12);

Rejecting a thenable fake

Rejecting a thenable fake will work exactly as expected, executing no then onSuccess functions, jumping straight to the catch.

const thenableFake = getThenableFake();

const thenStub = sinon.stub();
const catchStub = sinon.stub();

thenableFake
    .then(value => value + 1)
    .then(value => value * 2)
    .then(thenStub)
    .catch(() => null)
    .reject(new Error('Because.'));

assert.equal(thenStub.callCount, 0);
assert.equal(thenStub.args[0][0].message, 'Because.');

PromiseFake and Thenable APIs

PromiseFake API

// Getting a new instantiable PromiseFake -- required for test state safety
const PromiseFake = promiseDoubleFactory.getPromiseFake();

// Promise.all
const thenableFake = PromiseFake.all([ /* these promises are not processed. */ ]);

// Promise.race
const thenableFake = PromiseFake.race([ /* these promises are not processed. */ ]);

// Instantiation
const thenableProxyFake = new PromiseFake(function(resolve, reject){
    // Code goes here
    // Resolving and rejecting DOES NOT initiate actual promise resolution
});

// Available methods on thenableProxyFake:
thenableProxyFake.then();
thenableProxyFake.catch();
thenableProxyFake.finally();
thenableProxyFake.disableThrowOnNoCatch();

// Access to internal values:
const resolveArguments = thenableProxyFake.resolve.args;
const rejectArguments = thenableProxyFake.reject.args;

Thenable Fake API

// Getting a new thenable fake object
const thenableFake = promiseDoubleFactory.getThenableFake();

// Chaining thens, catches and finallys:
thenableFake
    .then(onSuccess1, onFailure1)
    .then(onSuccess2)
    .catch(onFailure2)
    .catch(onFailure3)
    .finally(onComplete1)
    .finally(onComplete2);

// Turning off errors when catch is missing on a thenable fake
thenableFake.disableThrowOnNoCatch();

// Resolve and execute onSuccess and onComplete functions completely:
thenableFake.resolve(arg1, arg2, ...);

// Reject and execute onFailure and onComplete functions completely:
thenableFake.reject(new Error('Something bad happened'));

Special Case and Analysis Step-wise Resolution

// Verify resolveNext can be called safely:
thenableFake.canResolve();

// Resolve onSuccess functions incrementally, calling onComplete functions upon last onSuccess execution completion:
const outcome = thenableFake.resolveNext(arg1, arg2, ...); // Returns outcome from internal execution

Version History

v1.0.0

Initial release:

  • PromiseFake
    • all
    • race
  • ThenableFake
    • then
    • catch
    • finally
    • resolve
    • resolveNext
    • reject