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);
});
});