ysmood / yaku

A lightweight promise library
https://tonicdev.com/ysmood/yaku
MIT License
290 stars 28 forks source link

Mock Mode #59

Closed lukeapage closed 4 years ago

lukeapage commented 6 years ago

Hi,

We use Yaku for production and had a need for a mock mode to allow synchronous tests.

I hacked it in: https://github.com/lukeapage/yaku-mock

If I cleaned it up, abstracted and made a PR back to you, would you be interested? or should I keep it as a fork?

ysmood commented 6 years ago

Actually, there's an easier approach, have you read this section of the doc?

https://github.com/ysmood/yaku#yakunexttick?

image

I'm not sure if this is what you want, but if you want to resolve promise synchronously without any side effect it should work for you.

ysmood commented 6 years ago

BTW, it's better to use the async resolution which will enforce you to think asynchronously and reduce some production bugs that caused by lacking asynchronous planning.

I have some experience with the testing of complex async projects. If you can tell me what's the trouble that makes you want to choose sync, I may help you to find a better way to test, other than hacking a primary lib.

lukeapage commented 6 years ago

thats a good point. I could try implementing a scheduler on top of this, which records setTimeout, then on flush, clears it and runs it immediately.

lukeapage commented 6 years ago

I have some experience with the testing of complex async projects. If you can tell me what's the trouble that makes you want to choose sync, I may help you to find a better way to test, other than hacking a primary lib.

A couple of years ago I had alot of problems with async tests where things finishing in different orders resulted in different results and then inconsistent tests. In those cases there were multiple setTimeout's with different times and not only was it inconsistent but slow (I realise that promises in particular don't suffer from long timeouts.. but if you start making calls sync it tends to affect everything).

So I prefer to mock out date, mock out timers like setTimeout and flush promises. It means you can have full control on the execution order and make one test in one order and one in another.

I wouldn't want to change nextTick to just be sync because it changes the behaviour e.g.

let a;
Promise.reject().catch(() => a());
a = () => console.log('only works async');

but what I do prefer to do is to treat my unit test like its a top level environment - e.g. once user code has returned to the test, if I run pending timers / flush promises then, I keep almost the same behaviour as a live environment, but control execution from my tests.

but your right, I have managed to implement this using nextTick:

// replace Promise with one with a Promise.flush function to make promises sync
import Yaku from 'yaku';

const queue = [];
let timeoutid = null;

function runQueue() {
    const q = queue.slice(0);
    queue.length = 0;
    for (let i = 0; i < q.length; i++) {
        q[i]();
    }
}

// Promises should run as normal but be able to be flushed
// as if the test ended and the setImmediate began
Yaku.nextTick = (fn) => {
    queue.push(fn);
    if (!timeoutid) {
        timeoutid = global.setImmediate(Yaku.flush);
    }
};

Yaku.flush = () => {
    while (queue.length) {
        runQueue();
    }
    global.clearImmediate(timeoutid);
    timeoutid = null;
};

Yaku.resolvedValue = (promise) => {
    let value;
    promise.then((resolvedValue) => {
        value = resolvedValue;
    });
    Yaku.flush();
    return value;
};

Yaku.isFulfilled = (promise) => {
    let isFulfilled = false;
    promise.then(function() {
        isFulfilled = true;
    });
    Yaku.flush();
    return isFulfilled;
};

Yaku.isRejected = (promise) => {
    let isRejected = false;
    promise.catch(function() {
        isRejected = true;
    });
    Yaku.flush();
    return isRejected;
};

// stop unhandled exceptions on Promise.reject which occurs often in the testing code
const originalPromiseReject = Yaku.reject;
Yaku.reject = (value) => {
    const promise = originalPromiseReject.call(Yaku, value);
    promise.catch(() => {});
    return promise;
};

export const isRejected = Yaku.isRejected;
export const isFulfilled = Yaku.isFulfilled;
export const resolvedValue = Yaku.resolvedValue;
export const flush = Yaku.flush;

global.Promise = Yaku;

and I am a bit more on the fence about this than I used to be - I test with jest and it does have pretty good support for promises, so I could probably keep promises async and use async tests, then use fake timers only to skip waiting for any setTimeouts.

btw in case your wondering why I mention setTimeouts so often - that is to mock rest requests and websocket data that come in at different times and orders.

ysmood commented 6 years ago

Actually what you want to do is have full control of the IO, I think you already know pure-function, for Yaku the only input IO to the outside world is nextTick, so if you take control of it you can make Yaku a pure module without any IO side effect.

If you want to go extremely, you'd better also mock all other common IOs, including File, Net, and Timer, then you even don't have to touch Yaku, you'll get what you want automatically because Yaku uses Timer internally.

If your system is fully inside a sandbox without any IO to outside world, you will get the best performance of a single CPU core for your tests. Normally people don't do that because IO is not always the bottleneck of the project, they prefer to put the time to more important problems.

If you want to leverage multiple CPU cores without all those common IOs, you might need to create a new test framework other than something like ava. Actually, I'm interested to create a perfect IO sandbox test framework for multiple CPU cores.

ysmood commented 6 years ago

// stop unhandled exceptions on Promise.reject which occurs often in the testing code const originalPromiseReject = Yaku.reject; Yaku.reject = (value) => { const promise = originalPromiseReject.call(Yaku, value); promise.catch(() => {}); return promise; };

About this code, I recommend you to read the doc of this section:

https://github.com/ysmood/yaku#yakuunhandledrejectionreason-p

nextTick is the only input IO of Yaku, unhandledRejection is the only output IO of Yaku, so you can use it to mute Yaku

ysmood commented 4 years ago

Close because of no activity