Last active
          January 5, 2024 15:36 
        
      - 
      
 - 
        
Save weshouman/98fbc490d729c946a9de63ca47aa966f to your computer and use it in GitHub Desktop.  
Revisions
- 
        
weshouman revised this gist
Jan 5, 2024 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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: 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)  - 
        
weshouman revised this gist
Jan 5, 2024 . 1 changed file with 5 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. **Note**: The original code which had the issue is left to ease comparisons, however the implementation with the `await Promise.resolve()` is not shown.  - 
        
weshouman revised this gist
Jan 5, 2024 . 1 changed file with 15 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,11 +1,18 @@ 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. ### Current Implementation 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. **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)  - 
        
weshouman revised this gist
Jan 5, 2024 . 7 changed files with 259 additions and 63 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,17 +1,37 @@ const enableFurtherWait = false; const callCount = 3; async function subFunction(index) { // 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)); // 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 <= 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: mainFunction completed at ${new Date().toISOString()}`); } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; 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 }); }); }); }); 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); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 }; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; } } }; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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(startTime = 0) { this.elapsedFakeTime = startTime; this.currentClock = sinon.useFakeTimers(this.elapsedFakeTime); this.isFakeTimerActive = true; return this.currentClock; }, pauseFakeTimer: function() { if (this.currentClock && this.isFakeTimerActive) { this.elapsedFakeTime = this.currentClock.now; this.currentClock.restore(); this.isFakeTimerActive = false; } }, resumeFakeTimer: function() { if (!this.isFakeTimerActive) { this.currentClock = sinon.useFakeTimers({ now: this.elapsedFakeTime }); this.isFakeTimerActive = true; } return this.currentClock; }, restoreRealTimer: function() { if (this.currentClock) { this.currentClock.restore(); this.currentClock = null; this.isFakeTimerActive = false; } } };  - 
        
weshouman created this gist
Dec 31, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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 }; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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); }); }); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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; } } };