Skip to content

Instantly share code, notes, and snippets.

@weshouman
Last active January 5, 2024 15:36
Show Gist options
  • Save weshouman/98fbc490d729c946a9de63ca47aa966f to your computer and use it in GitHub Desktop.
Save weshouman/98fbc490d729c946a9de63ca47aa966f to your computer and use it in GitHub Desktop.

Revisions

  1. weshouman revised this gist Jan 5, 2024. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -17,5 +17,5 @@ Initially jumping to the real time was avoided as the promise was not resolved,

    ### References

    - [StackOverflow Question](https://stackoverflow.com/q/77739770/2730737)
    - [GitHub Issue](https://github.com/sinonjs/fake-timers/issues/487)
    - [StackOverflow Question: Switching between fake and real time using sinon.js](https://stackoverflow.com/q/77739770/2730737)
    - [GitHub Issue: Cleanly switch to real time and back again to fake time while testing #487 ](https://github.com/sinonjs/fake-timers/issues/487)
  2. weshouman revised this gist Jan 5, 2024. 1 changed file with 5 additions and 2 deletions.
    7 changes: 5 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,7 @@
    ## Testing in Both Real and Fake Time in Javascript

    The code shows how to jump between the fake and real time allowing to:

    - Execute real-time dependent stubs (in real time), in this scenario it's the HIL simulation.
    - Execute long test in time controlled manner (in fake time), in this scenario it's the HIL test.

    @@ -7,8 +10,8 @@ The code shows how to jump between the fake and real time allowing to:
    Avoid using `await Promise.resolve()` by ticking asynchronously, for example using `fakeClock.tickAsync()` and then you could move between fake and real time, the code is updated to show this solution.

    ### Previous Implementation
    Initially jumping to the real time was avoided as the promise was not resolved, thus it became necessary to use `await Promise.resolve()`, however not moving to the real time would come at the cost of granual control, for example in this demo HIL simulation won't be posssible.
    **Note**: It could be necessary to use `await Promise.resolve()` a couple of times, based on the number of promises we want to resolve.
    Initially jumping to the real time was avoided as the promise was not resolved, thus it became necessary to use `await Promise.resolve()`, however not moving to the real time would come at the cost of granual control, for example in this demo HIL simulation won't be posssible.
    **Note**: It could be necessary to use `await Promise.resolve()` a couple of times, based on the number of promises we want to resolve.

    **Note**: The original code which had the issue is left to ease comparisons, however the implementation with the `await Promise.resolve()` is not shown.

  3. weshouman revised this gist Jan 5, 2024. 1 changed file with 15 additions and 8 deletions.
    23 changes: 15 additions & 8 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,18 @@
    This snippet is a demo for time-dependent testing of async subfunctions while considering the expected complexity of delays and nesting
    and the desire for having fine-control for the resolution of each stub.
    The code shows how to jump between the fake and real time allowing to:
    - Execute real-time dependent stubs (in real time), in this scenario it's the HIL simulation.
    - Execute long test in time controlled manner (in fake time), in this scenario it's the HIL test.

    Note:
    - It's possible to avoid the conditional waiting by using `await Promise.resolve()`, however that would come at the cost of granual control
    - `await Promise.resolve()` could be needed to be executed a couple of time, based on the number of promises we want to resolve.
    ### Current Implementation

    ### Update
    Avoid using `await Promise.resolve()` by ticking asynchronously, for example using `fakeClock.tickAsync()` and then you could move between fake and real time easily, the code is updated to show this solution.
    Avoid using `await Promise.resolve()` by ticking asynchronously, for example using `fakeClock.tickAsync()` and then you could move between fake and real time, the code is updated to show this solution.

    The original code is left to ease comparisons
    ### Previous Implementation
    Initially jumping to the real time was avoided as the promise was not resolved, thus it became necessary to use `await Promise.resolve()`, however not moving to the real time would come at the cost of granual control, for example in this demo HIL simulation won't be posssible.
    **Note**: It could be necessary to use `await Promise.resolve()` a couple of times, based on the number of promises we want to resolve.

    **Note**: The original code which had the issue is left to ease comparisons, however the implementation with the `await Promise.resolve()` is not shown.

    ### References

    - [StackOverflow Question](https://stackoverflow.com/q/77739770/2730737)
    - [GitHub Issue](https://github.com/sinonjs/fake-timers/issues/487)
  4. weshouman revised this gist Jan 5, 2024. 7 changed files with 259 additions and 63 deletions.
    4 changes: 4 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -5,3 +5,7 @@ Note:
    - It's possible to avoid the conditional waiting by using `await Promise.resolve()`, however that would come at the cost of granual control
    - `await Promise.resolve()` could be needed to be executed a couple of time, based on the number of promises we want to resolve.

    ### Update
    Avoid using `await Promise.resolve()` by ticking asynchronously, for example using `fakeClock.tickAsync()` and then you could move between fake and real time easily, the code is updated to show this solution.

    The original code is left to ease comparisons
    30 changes: 25 additions & 5 deletions UUT.js
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,37 @@

    const enableFurtherWait = false;
    const callCount = 3;

    async function subFunction(index) {
    console.log(`UUT: Starting subfunction ${index} at ${new Date().toISOString()}`);
    // if stubbed, this should not be called
    console.log(`UUT: Starting real subfunction ${index} at ${new Date().toISOString()}`);
    // Simulate an asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 1000));
    console.log(`UUT: Completed subfunction ${index} at ${new Date().toISOString()}`);
    // if stubbed, this should not be called
    console.log(`UUT: Completed real subfunction ${index} at ${new Date().toISOString()}`);
    }

    function unexpectedTimeJump() {
    var currentDate = new Date();
    var thresholdDate = new Date('2000-01-01');

    if (currentDate > thresholdDate) {
    throw new Error('Current date is newer than January 1, 2000');
    }
    }

    async function mainFunction() {
    for (let i = 1; i <= 5; i++) {
    for (let i = 1; i <= callCount; i++) {
    // await subFunction(i); // this won't work
    console.log(`UUT: Invoking subfunction ${i} at ${new Date().toISOString()}`);
    await UUT.subFunction(i);
    console.log(`UUT: Invoked subfunction ${i} at ${new Date().toISOString()}`);
    unexpectedTimeJump()
    }
    if (enableFurtherWait) {
    console.log(`UUT: Waiting a couple of seconds after subfunctions at ${new Date().toISOString()}`);
    await new Promise((resolve) => { console.log("Promise started"); setTimeout(resolve, 2000);}); // Wait for 2 seconds
    }
    console.log(`UUT: Waiting a couple of seconds after subfunctions at ${new Date().toISOString()}`);
    await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
    console.log(`UUT: mainFunction completed at ${new Date().toISOString()}`);
    }

    146 changes: 98 additions & 48 deletions UUT.test.js
    Original file line number Diff line number Diff line change
    @@ -5,57 +5,107 @@ import sinon from 'sinon';
    import { expect } from 'chai';

    const promiseResolvers = [];
    const useGlobalClock = false;
    let clock;
    const callCount = 3;

    describe('Main Function Test', function() {
    beforeEach(function() {
    timerUtils.useFakeTimer();
    console.log(UUT.subFunction)
    sinon.stub(UUT, 'subFunction').callsFake((index) => {
    console.log(`Stub: subFunction ${index} called, ${new Date().toISOString()}`);
    return new Promise((resolve) => {
    promiseResolvers.push(resolve);
    });
    const HIL_SIM_TIME = 1000;

    async function realTimeOut(ms) {
    if (useGlobalClock) {
    clock.restore()
    } else {
    timerUtils.pauseFakeTimer();
    }

    await new Promise(resolve => setTimeout(resolve, ms));

    if (useGlobalClock) {
    clock = sinon.useFakeTimers();
    } else {
    timerUtils.resumeFakeTimer();
    }
    }

    describe('Main Function Test', function () {
    beforeEach(function () {
    if (useGlobalClock) {
    clock = sinon.useFakeTimers();
    } else {
    timerUtils.useFakeTimer();
    }

    sinon.stub(UUT, 'subFunction').callsFake(async (index) => {
    console.log(`Stub: subFunction ${index} called, ${new Date().toISOString()}`);
    return new Promise((resolve) => {
    promiseResolvers.push(() => {
    // HIL Simulation calls are stubbed here
    timerUtils.pauseFakeTimer(); // Pause the fake timer
    console.log(`Stub: [HIL-SIM] call #${index} starting on ${new Date().toISOString()}`)
    setTimeout(() => {
    resolve();
    console.log(`Stub: [HIL-SIM] call #${index} ended on ${new Date().toISOString()}`)
    timerUtils.resumeFakeTimer(); // Resume the fake timer
    }, HIL_SIM_TIME); // Real-time timeout
    });
    console.log(UUT.subFunction)
    });
    });

    afterEach(function() {
    timerUtils.restoreRealTimer();
    promiseResolvers.length = 0;
    UUT.subFunction.restore();
    });
    });

    it('should complete mainFunction with controlled promise resolution', async function() {
    const mainFunctionPromise = UUT.mainFunction();
    let clock;
    // Ensure we advance time and resolve promises only after they are pushed
    for (let i = 1; i <= 5; i++) {
    console.log(`Test: Advancing fake timer for subfunction ${i}`);
    timerUtils.currentClock.tick(1000); // Advance time for each subfunction

    timerUtils.pauseFakeTimer();
    await new Promise(resolve => setTimeout(resolve, 50));
    // This does not resume the timer
    timerUtils.resumeFakeTimer();
    // This resumes the timer
    // clock = sinon.useFakeTimers();

    console.log(`Test: Resolving subfunction ${i}`);
    console.log(`Resolvers count ${promiseResolvers.length}, resolving at index: ${i-1}`)
    if (typeof promiseResolvers[i - 1] === 'function') {
    promiseResolvers[i - 1](); // Resolve the i-th subfunction's promise
    console.log("resolved")
    } else {
    throw new Error(`Resolver for subfunction ${i} is not a function`);
    }

    }

    console.log('Test: All subfunctions resolved, advancing time for the final wait');
    timerUtils.currentClock.tick(2000); // Advance time for the final 2-second wait

    await mainFunctionPromise;
    console.log('Test: mainFunction should be completed now');
    expect(UUT.subFunction.callCount).to.equal(5);
    });
    afterEach(function () {
    if (useGlobalClock) {
    clock.restore();
    } else {
    timerUtils.restoreRealTimer();
    }
    promiseResolvers.length = 0;
    UUT.subFunction.restore();
    });

    it('should complete mainFunction with controlled promise resolution', async function () {
    this.timeout((callCount + 2) * HIL_SIM_TIME);
    const mainFunctionPromise = UUT.mainFunction();
    // Ensure we advance time and resolve promises only after they are pushed
    for (let i = 1; i <= callCount; i++) {

    // Wait for real time based stub, at least HIL_SIM_TIME
    console.log(`Test: Start waiting in real time (for the stub) [subfunction ${i}]`);
    await realTimeOut(1000);
    console.log(`Test: Finish waiting in real time (for the stub) [subfunction ${i}]`);

    console.log(`Test: Start waiting in fake time (for the UUT) [subfunction ${i}]`);
    if (useGlobalClock) {
    await clock.tickAsync(1000)
    } else {
    await timerUtils.currentClock.tickAsync(1000);
    }
    console.log(`Test: Finish waiting in fake time (for the UUT) [subfunction ${i}]`);

    let rCount = promiseResolvers.length;
    expect(rCount, `Expected ${i} resolvers but received ${rCount}`).to.equal(i);

    console.log(`Test: Resolving subfunction ${i}`);
    if (typeof promiseResolvers[i - 1] === 'function') {
    promiseResolvers[i - 1](); // Resolve the i-th subfunction's promise
    console.log(`Test: Resolved subfunction ${i}`)
    } else {
    // This should not be reached as the previous expectation should fire
    throw new Error(`Test: Resolver for subfunction ${i} is not a function`);
    }
    }

    console.log(`Test: All ${promiseResolvers.length} subfunction promises are resolved`);
    console.log('Test: Advancing time for the final wait');
    if (useGlobalClock) {
    await clock.tickAsync(4000)
    } else {
    await timerUtils.currentClock.tickAsync(4000);
    }

    console.log('Test: awaiting mainFunction promise');
    await mainFunctionPromise;
    console.log('Test: mainFunction should be completed now');
    expect(UUT.subFunction.callCount).to.equal(callCount);
    });
    });
    22 changes: 22 additions & 0 deletions old_UUT.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    async function subFunction(index) {
    console.log(`UUT: Starting subfunction ${index} at ${new Date().toISOString()}`);
    // Simulate an asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 1000));
    console.log(`UUT: Completed subfunction ${index} at ${new Date().toISOString()}`);
    }

    async function mainFunction() {
    for (let i = 1; i <= 5; i++) {
    // await subFunction(i); // this won't work
    await UUT.subFunction(i);
    }
    console.log(`UUT: Waiting a couple of seconds after subfunctions at ${new Date().toISOString()}`);
    await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
    console.log(`UUT: mainFunction completed at ${new Date().toISOString()}`);
    }

    export const UUT = {
    mainFunction,
    subFunction
    };

    61 changes: 61 additions & 0 deletions old_UUT.test.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    // UUT.test.js
    import { timerUtils } from './old_timerUtils.js';
    import { UUT } from './old_UUT.js';
    import sinon from 'sinon';
    import { expect } from 'chai';

    const promiseResolvers = [];

    describe('Main Function Test', function() {
    beforeEach(function() {
    timerUtils.useFakeTimer();
    console.log(UUT.subFunction)
    sinon.stub(UUT, 'subFunction').callsFake((index) => {
    console.log(`Stub: subFunction ${index} called, ${new Date().toISOString()}`);
    return new Promise((resolve) => {
    promiseResolvers.push(resolve);
    });
    });
    console.log(UUT.subFunction)
    });

    afterEach(function() {
    timerUtils.restoreRealTimer();
    promiseResolvers.length = 0;
    UUT.subFunction.restore();
    });

    it('should complete mainFunction with controlled promise resolution', async function() {
    const mainFunctionPromise = UUT.mainFunction();
    let clock;
    // Ensure we advance time and resolve promises only after they are pushed
    for (let i = 1; i <= 5; i++) {
    console.log(`Test: Advancing fake timer for subfunction ${i}`);
    timerUtils.currentClock.tick(1000); // Advance time for each subfunction

    timerUtils.pauseFakeTimer();
    await new Promise(resolve => setTimeout(resolve, 50));
    // This does not resume the timer
    timerUtils.resumeFakeTimer();
    // This resumes the timer
    // clock = sinon.useFakeTimers();

    console.log(`Test: Resolving subfunction ${i}`);
    console.log(`Resolvers count ${promiseResolvers.length}, resolving at index: ${i-1}`)
    if (typeof promiseResolvers[i - 1] === 'function') {
    promiseResolvers[i - 1](); // Resolve the i-th subfunction's promise
    console.log("resolved")
    } else {
    throw new Error(`Resolver for subfunction ${i} is not a function`);
    }

    }

    console.log('Test: All subfunctions resolved, advancing time for the final wait');
    timerUtils.currentClock.tick(2000); // Advance time for the final 2-second wait

    await mainFunctionPromise;
    console.log('Test: mainFunction should be completed now');
    expect(UUT.subFunction.callCount).to.equal(5);
    });
    });
    34 changes: 34 additions & 0 deletions old_timerUtils.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    import sinon from 'sinon';

    export const timerUtils = {
    currentClock: null,
    elapsedFakeTime: 0,

    useFakeTimer: function() {
    console.log('Starting fake timer');
    this.currentClock = sinon.useFakeTimers();
    this.elapsedFakeTime = 0;
    },

    pauseFakeTimer: function() {
    if (this.currentClock) {
    this.elapsedFakeTime = this.currentClock.now;
    console.log('Pausing fake timer at:', this.elapsedFakeTime);
    this.currentClock.restore();
    }
    },

    resumeFakeTimer: function() {
    console.log('Resuming fake timer from:', this.elapsedFakeTime);
    this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime });
    },

    restoreRealTimer: function() {
    if (this.currentClock) {
    console.log('Restoring real timer');
    this.currentClock.restore();
    this.currentClock = null;
    }
    }
    };

    25 changes: 15 additions & 10 deletions timerUtils.js
    Original file line number Diff line number Diff line change
    @@ -3,32 +3,37 @@ import sinon from 'sinon';
    export const timerUtils = {
    currentClock: null,
    elapsedFakeTime: 0,
    // Make the timer restoration safe for idempotency
    isFakeTimerActive: false,

    useFakeTimer: function() {
    console.log('Starting fake timer');
    this.currentClock = sinon.useFakeTimers();
    this.elapsedFakeTime = 0;
    useFakeTimer: function(startTime = 0) {
    this.elapsedFakeTime = startTime;
    this.currentClock = sinon.useFakeTimers(this.elapsedFakeTime);
    this.isFakeTimerActive = true;
    return this.currentClock;
    },

    pauseFakeTimer: function() {
    if (this.currentClock) {
    if (this.currentClock && this.isFakeTimerActive) {
    this.elapsedFakeTime = this.currentClock.now;
    console.log('Pausing fake timer at:', this.elapsedFakeTime);
    this.currentClock.restore();
    this.isFakeTimerActive = false;
    }
    },

    resumeFakeTimer: function() {
    console.log('Resuming fake timer from:', this.elapsedFakeTime);
    this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime });
    if (!this.isFakeTimerActive) {
    this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime });
    this.isFakeTimerActive = true;
    }
    return this.currentClock;
    },

    restoreRealTimer: function() {
    if (this.currentClock) {
    console.log('Restoring real timer');
    this.currentClock.restore();
    this.currentClock = null;
    this.isFakeTimerActive = false;
    }
    }
    };

  5. weshouman created this gist Dec 31, 2023.
    7 changes: 7 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    This snippet is a demo for time-dependent testing of async subfunctions while considering the expected complexity of delays and nesting
    and the desire for having fine-control for the resolution of each stub.

    Note:
    - It's possible to avoid the conditional waiting by using `await Promise.resolve()`, however that would come at the cost of granual control
    - `await Promise.resolve()` could be needed to be executed a couple of time, based on the number of promises we want to resolve.

    22 changes: 22 additions & 0 deletions UUT.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    async function subFunction(index) {
    console.log(`UUT: Starting subfunction ${index} at ${new Date().toISOString()}`);
    // Simulate an asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 1000));
    console.log(`UUT: Completed subfunction ${index} at ${new Date().toISOString()}`);
    }

    async function mainFunction() {
    for (let i = 1; i <= 5; i++) {
    // await subFunction(i); // this won't work
    await UUT.subFunction(i);
    }
    console.log(`UUT: Waiting a couple of seconds after subfunctions at ${new Date().toISOString()}`);
    await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
    console.log(`UUT: mainFunction completed at ${new Date().toISOString()}`);
    }

    export const UUT = {
    mainFunction,
    subFunction
    };

    61 changes: 61 additions & 0 deletions UUT.test.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    // UUT.test.js
    import { timerUtils } from './timerUtils.js';
    import { UUT } from './UUT.js';
    import sinon from 'sinon';
    import { expect } from 'chai';

    const promiseResolvers = [];

    describe('Main Function Test', function() {
    beforeEach(function() {
    timerUtils.useFakeTimer();
    console.log(UUT.subFunction)
    sinon.stub(UUT, 'subFunction').callsFake((index) => {
    console.log(`Stub: subFunction ${index} called, ${new Date().toISOString()}`);
    return new Promise((resolve) => {
    promiseResolvers.push(resolve);
    });
    });
    console.log(UUT.subFunction)
    });

    afterEach(function() {
    timerUtils.restoreRealTimer();
    promiseResolvers.length = 0;
    UUT.subFunction.restore();
    });

    it('should complete mainFunction with controlled promise resolution', async function() {
    const mainFunctionPromise = UUT.mainFunction();
    let clock;
    // Ensure we advance time and resolve promises only after they are pushed
    for (let i = 1; i <= 5; i++) {
    console.log(`Test: Advancing fake timer for subfunction ${i}`);
    timerUtils.currentClock.tick(1000); // Advance time for each subfunction

    timerUtils.pauseFakeTimer();
    await new Promise(resolve => setTimeout(resolve, 50));
    // This does not resume the timer
    timerUtils.resumeFakeTimer();
    // This resumes the timer
    // clock = sinon.useFakeTimers();

    console.log(`Test: Resolving subfunction ${i}`);
    console.log(`Resolvers count ${promiseResolvers.length}, resolving at index: ${i-1}`)
    if (typeof promiseResolvers[i - 1] === 'function') {
    promiseResolvers[i - 1](); // Resolve the i-th subfunction's promise
    console.log("resolved")
    } else {
    throw new Error(`Resolver for subfunction ${i} is not a function`);
    }

    }

    console.log('Test: All subfunctions resolved, advancing time for the final wait');
    timerUtils.currentClock.tick(2000); // Advance time for the final 2-second wait

    await mainFunctionPromise;
    console.log('Test: mainFunction should be completed now');
    expect(UUT.subFunction.callCount).to.equal(5);
    });
    });
    34 changes: 34 additions & 0 deletions timerUtils.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    import sinon from 'sinon';

    export const timerUtils = {
    currentClock: null,
    elapsedFakeTime: 0,

    useFakeTimer: function() {
    console.log('Starting fake timer');
    this.currentClock = sinon.useFakeTimers();
    this.elapsedFakeTime = 0;
    },

    pauseFakeTimer: function() {
    if (this.currentClock) {
    this.elapsedFakeTime = this.currentClock.now;
    console.log('Pausing fake timer at:', this.elapsedFakeTime);
    this.currentClock.restore();
    }
    },

    resumeFakeTimer: function() {
    console.log('Resuming fake timer from:', this.elapsedFakeTime);
    this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime });
    },

    restoreRealTimer: function() {
    if (this.currentClock) {
    console.log('Restoring real timer');
    this.currentClock.restore();
    this.currentClock = null;
    }
    }
    };