Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test parallel, mocked data requests in JEST whilst simulating cached responses with a 500ms threshold

The purpose of the tests is to mock parallel requests that fetch different sources of data. I introduce an artificial latency for each request and after some time return a simple string with an identifying digit to see if the data has been loaded from cache (requests within 500ms) or not. So for data loaded within 500ms the output should be "A1B1", else, after 500ms, it should be "A2B2" and so on.

// index.test.js
const { wait } = require('./util/wait.js');
const { requestAandB, requestBandC } = require('./index.js');

test('Test cache timings ', () => Promise.all([

  // send two requests in parallel after 0 ms (immediately)
  wait(0).then(() => Promise.all([
    expect(requestAandB()).resolves.toEqual('A1B1'),
    expect(requestBandC()).resolves.toEqual('B1C1'),
  ])),

  // send two requests in parallel after 480 ms
  wait(480).then(() => Promise.all([
    expect(requestAandB()).resolves.toEqual('A1B1'),
    expect(requestBandC()).resolves.toEqual('B1C1'),
  ])),

  // send two requests in parallel after 520 ms
  wait(520).then(() => Promise.all([
    expect(requestAandB()).resolves.toEqual('A2B2'),
    expect(requestBandC()).resolves.toEqual('B2C2'),
  ])),

]));

This is how I mock the data loads

// get-data.js

async function mockLoading(str) {
  // introduce some latency
  const waitDuration = Math.round(Math.random() * (WAIT_MAX - WAIT_MIN)) + WAIT_MIN;
  await wait(waitDuration);
  // create or increase counter every time the function is being called
  counters[str] = counters[str] === undefined ? 1 : counters[str] + 1;
  return str + counters[str];
}

module.exports = {
  loadDataA: async () => mockLoading('A'),
  loadDataB: async () => mockLoading('B'),
  loadDataC: async () => mockLoading('C'),
}

Finally, the implementation for the methods requestAandB and requestBandC imported in the test file:

const { loadDataA, loadDataB, loadDataC } = require('./model/get-data.js');
    
const all = Promise.all([loadDataA(), loadDataB(), loadDataC()])

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}  
   
async function requestAandB() {
  const temp = await all

  await delay(Math.random() * 510)
 
  return temp.filter((_, i) =>  i < 2).join("")

}

async function requestBandC() {
  const temp = await all
 
  await delay(Math.random() * 510)

  return temp.filter((_, i) => i > 0).join("") 

}

module.exports = { requestAandB, requestBandC }

Tests for the data "A1B1", "B1C1" are fine, but because the latency (in the mockLoading function) is always above the 500ms threshold, I am not able to get the right results for data returned after that. In effect, "A2B2" and "B2C2" always fail.

Does anyone know what am I missing here?

like image 786
rags2riches-prog Avatar asked Jun 18 '21 07:06

rags2riches-prog


People also ask

How do I mock the response in a jest test?

Use .mockResolvedValue (<mocked response>) to mock the response. That's it! Here's what our test looks like after doing this: What's going on here? Let's break this down. The most important part to understand here is the import and jest.mock ():

Is it possible to mock network requests in React Native with jest?

In React Native I use fetchto perform network requests, however fetchis not an explicitly required module, so it is seemingly impossible to mock in Jest. Even trying to call a method which uses fetchin a test will result in: ReferenceError: fetch is not defined Is there a way to test such API requests in react native with Jest?

What is the difference between beforeall and restoreallmocks in jest?

jest.restoreAllMocks (): It restores all mocks back to their original value. In addition to jest.resetAllMocks (), it also restores the original (non-mocked) implementation. Jest also provides a number of APIs to setup and teardown tests. beforeAll (fn): It runs a function before any of the tests in this file run.

What is the difference between before and aftereach in jest?

beforeEach (fn): It runs a function before each of the tests in this file runs. afterEach (fn): It runs a function after each one of the tests in this file completes. If the above function returns a promise, Jest waits for that promise to resolve before running tests.


2 Answers

It looks like your first tests don't work properly as well.

So for data loaded within 500ms the output should be "A1B1", else, after 500ms, it should be "A2B2" and so on.

But later you said

Tests for the data "A1B1", "B1C1" are fine, but because the latency (in the mockLoading function) is always above the 500ms threshold, I am not able to get the right results for data returned after that. In effect, "A2B2" and "B2C2" always fail.

Easiest way will be to pass time to requestAandB and requestBandC, because using Math.random() can give you random numbers and might fail tests randomly. So try this approach:

test('Test cache timings ', () =>
  Promise.all([
    // send two requests in parallel after 0 ms (immediately)
    expect(requestAandB(0)).resolves.toEqual('A1B1'),
    expect(requestBandC(0)).resolves.toEqual('B1C1'),

    // send two requests in parallel after 480 ms
    expect(requestAandB(480)).resolves.toEqual('A1B1'),
    expect(requestBandC(480)).resolves.toEqual('B1C1'),

    // send two requests in parallel after 520 ms
    expect(requestAandB(520)).resolves.toEqual('A2B2'),
    expect(requestBandC(520)).resolves.toEqual('B2C2'),

    // send two requests in parallel after 360 ms
    expect(requestAandB(360)).resolves.toEqual('A1B1'),
    expect(requestBandC(360)).resolves.toEqual('B1C1'),

    // send two requests in parallel after 750 ms
    expect(requestAandB(750)).resolves.toEqual('A2B2'),
    expect(requestBandC(750)).resolves.toEqual('B2C2'),
  ]));

index.js

const { loadDataA, loadDataB, loadDataC } = require('./get-data.js');

async function requestAandB(time) {
  const temp = await Promise.all([
    loadDataA(time),
    loadDataB(time),
    loadDataC(time),
  ]); // don't use global Promise outside of this function

  return temp.filter((_, i) => i < 2).join('');
}

async function requestBandC(time) {
  const temp = await Promise.all([
    loadDataA(time),
    loadDataB(time),
    loadDataC(time),
  ]); // don't use global Promise outside of this function

  return temp.filter((_, i) => i > 0).join('');
}

module.exports = { requestAandB, requestBandC };

get-data.js

// get-data.js
const { wait } = require('./wait.js');

async function mockLoading(time, str) {
  await wait(time);

  // here is the logic if time is less then 500ms then append 1 else append 2
  let label = str;
  if (Math.ceil(time / 500) <= 1) {
     label += '1';
  } else {
     label += '2';
  }

  return label;
}

module.exports = {
  loadDataA: async time => mockLoading(time, 'A'),
  loadDataB: async time => mockLoading(time, 'B'),
  loadDataC: async time => mockLoading(time, 'C'),
};
like image 143
Observer Avatar answered Sep 30 '22 21:09

Observer


This was a tricky problem indeed but I managed to crack it by doing the following:

  1. Moving the Promise.all inside their respective functions; that is because the promise for the loadData functions was only called once on module load (thanks to @Teneff and @Observer for the hints)

  2. Implementation of a cache function to cache the intended promise (not the intended outcome...), returning the loadData (cached) once some condition was met.

We did not need to change the mockLoading function at all. The OP required the implementation of client code, calling then external API's to simulate high variance across n number of asynchronous requests. Put it differently, we had to eliminate (or minimise) the risk of data collision for parallel asynchronous requests, and how to test some mocked cases in Jest.

Some code to make the whole process clearer:

const { loadDataA, loadDataB, loadDataC } = require('./model/get-data.js');

// we tackle this problem using memoization
// when `useCache` is called, we cache the loadData function name
// we then assign 1. a timestamp property to it and 2. a pointer to the promise itself

const CACHE_DURATION = 500;

const cache = {};
 function useCache(fn) {
   const cached = cache[fn.name];
   const now = Date.now();
   if (!cached || cached.ts < now - CACHE_DURATION) {
     cache[fn.name] = { ts: now, promise: fn() }
   }
   return cache[fn.name].promise;
 }
 
 async function requestAandB() {
   const [a, b] = await Promise.all([ loadDataA, loadDataB ].map(useCache));
   return a + b;
 }
 
 async function requestBandC() {
   const [b, c] = await Promise.all([ loadDataB, loadDataC ].map(useCache));
   return b + c;
 }

module.exports = { requestAandB, requestBandC }

Thank you

like image 26
rags2riches-prog Avatar answered Sep 30 '22 19:09

rags2riches-prog