Utils for coroutines in Javascript


Keywords
generator, cocoutine, yield, test, coroutines
License
MIT
Install
npm install cocorico@1.1.3

Documentation

cocorico

Test untestable code with coroutines/generators.

Remove side effects in your tests !

Why ?

You want :

  • to unit test a function with side effects without executing them.
  • to unit test a function without trigger some function calls

Here is a solution :

  • Migrate your function to a coroutine/generator
  • Identify and wrap each dependency (side effect, function call or statement) in yield function | { fn: Function, args: [arg1, arg2, ...]}
  • Use your generator in your test and mock with ease side effects with generator.next(arg) function
  • Create a version of your function that performs side effects by wrapping your function with cocorico. This one will be used by your app code.

Installation

npm install cocorico

How to use it ?

  • cocorico: Generator -> () -> Promise

Call cocorico() with a coroutine/generator. It will return a function that returns a promise.

const cocorico = require('cocorico');
const { call } = cocorico;

const fakeApiFetch = (arg1, arg2) => ...

let store;
// This function will be used in your test
function* CoDoSomeStuff() {
  let arg1 = ...;
  let arg2 = ...;

  // 1st option, yield a function
  const a = yield () => fakeApiFetch(arg1, arg2);
  // 2nd option, yield a call 
  const b = yield call(fakeApiFetch, arg1, arg2);

  yield () => {
    store = a + b;
  };

  return a + b + 1;
}

// This function will be used by your app code
const doSomeStuff = cocorico(CoDoSomeStuff);

// Like this for example.
return doSomeStuff().then((r) => {
  console.log(store); // 2
  console.log(r); // 3
})
  • cocorico.call : (fn, ...args) -> { fn: Function, args: [...args] }

Useful to assert fn arguments in unit tests. It returns an object with fn and args properties. Wrapped in cocorico, yielded objects are transformed in functions and executed.

If you don't need to assert arguments, write your effects just by yielding a function, like this: yield () => fn(arg1, arg2)

Example of a coroutine using call:

const cocorico = require('cocorico');
const { call } = cocorico;

function* CoDoSomeStuff() {
  ...
  // When CoDoSomeStuff is executed like this
  // cocorico(CoDoSomeStuff)()
  // This line is equivalent to `await fn(1, 2)`
  yield call(fn, 1, 2);
  ...
}

And this the corresponding test:

it('CoDoSomeStuff', () => {
  const co = CoDoSomeStuff();
  const callObj = co.next();
  expect(callObj.value.args).toEqual([1, 2]);
});

How to migrate your function to a generator

You have an async/await function

with Async/Await with Generator and cocorico
async function doSomeStuff() {
  const a = await fakeApiFetch(options);
  const b = await fakeApiFetch(options);
  return a + b + 1;
}
function* CoDoSomeStuff() {
  const a = yield call(fakeApiFetch, options);
  const b = yield call(fakeApiFetch, options);
  return a + b + 1;
}
const doSomeStuff = cocorico(CoDoSomeStuff);

Migration step by step

Instructions to migrate to generator with cocorico version :

// 1. Add cocorico
import cocorico, { call } from 'cocorico';
// or
// const cocorico = require('cocorico');
// const { call } = cocorico;

// 2. Modify signature: 
// - Remove async keyword and add * just after function keyword.
// - Prefix function name with Co to identify coroutine easily
function* CoDoSomeStuff() {
  // 3. For each await instruction :
  // - Replace await by yield
  // - Wrap code to the right of await in a lambda or in call
  const a = yield call(fakeApiFetch, options); // Dependency 1
  const b = yield call(fakeApiFetch, options); // Dependency 2
  return a + b + 1;
}

// 4. Create a version that performs side effects with cocorico

const doSomeStuff = cocorico(CoDoSomeStuff);

Now, CoDoSomeStuff() can be tested.

The GOAL of this test is to test inner logic of CoDoSomeStuff() without testing its dependencies.

These ones are identified by yield instructions.

describe('test with cocorico', () => {
  it('should works', () => {
    // Get coroutine iterator
    const co = CoDoSomeStuff();
    
    // execute coroutine until next yield (1st yield)
    co.next();
    co.next(3); 
    const r = co.next(5);

    expect(r.value).toBe(9);
  });
});

By calling co.next(3), it evaluates expression yield call(fakeApiFetch, options) to 3.

By calling co.next(5), it evaluates expression yield call(fakeApiFetch, options) to 5.

So, last call of co.next returns value returned by CoDoSomeStuff() and it can asserted easily with expect(r.value).toBe(9);

How to write a test on a generator/coroutine

Here is an example of coroutine/generator :

function* CoDoSomeStuff() {
  const a = yield () => fakeApiFetch();
  const b = yield () => fakeApiFetch();
  return a + b + 1;
}

Here is the corresponding test :

describe('test with cocorico', () => {
  it('should works', () => {
    // Get coroutine iterator
    const co = CoDoSomeStuff();
    
    // execute coroutine until next yield (1st yield)
    co.next();
    
    // specify returned value (3) from 1st yield
    // and execute code until next yield (2nd)
    co.next(3); 

    // specify returned value (5) from 2nd yield
    // and execute code until the end (no more yield)
    const r = co.next(5);

    expect(r.value).toBe(9);
  });
});