How do I setup this JS code to do better testing?

140 views Asked by At

Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.

const $ = require('jquery')
const axios = require('axios')

let workerComplete = () => {
  window.location.reload()
}

async function checkWorkerStatus() {
  const worker_id = $(".worker-waiter").data('worker-id')
  const response = await axios.get(`/v1/workers/${worker_id}`)
  return response.data
}

function waitForWorker() {
  if (!$('.worker-waiter').length) {
    return
  }

  checkWorkerStatus().then(data => {
      // delay next action by 1 second e.g. calling api again
      return new Promise(resolve => setTimeout(() => resolve(data), 1000));
    }).then(worker_response => {
    const working_statuses = ['queued', 'working']
    if (worker_response && working_statuses.includes(worker_response.status)) {
      waitForWorker()
    } else {
      workerComplete()
    }
  })
}

export {
  waitForWorker,
  checkWorkerStatus,
  workerComplete
}

if (process.env.NODE_ENV !== 'test') $(waitForWorker)

Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'

import axios from 'axios'
import * as Worker from 'worker_waiter'

jest.mock('axios')

beforeAll(() => {
  Object.defineProperty(window, 'location', {
    value: { reload: jest.fn() }
  })
});

beforeEach(() => jest.resetAllMocks() )

afterEach(() => {
  jest.restoreAllMocks();
});

describe('worker is complete after 2 API calls a', () => {
  const worker_id = Math.random().toString(36).slice(-5) // random string

  beforeEach(() => {
      axios.get
      .mockResolvedValueOnce({ data: { status: 'working' } })
      .mockResolvedValueOnce({ data: { status: 'complete' } })
      jest.spyOn(Worker, 'waitForWorker')
      jest.spyOn(Worker, 'checkWorkerStatus')
    document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
  })

  it('polls the correct endpoint twice a', async() => {
    const endpoint = `/v1/workers/${worker_id}`

    await Worker.checkWorkerStatus().then((data) => {
      expect(axios.get.mock.calls).toMatchObject([[endpoint]])
      expect(data).toMatchObject({"status": "working"})
    })
    await Worker.checkWorkerStatus().then((data) => {
      expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
      expect(data).toMatchObject({"status": "complete"})
    })
  })


  it('polls the correct endpoint twice b', async() => {
    jest.mock('waitForWorker', () => {
      expect(Worker.checkWorkerStatus).toBeCalled()
    })
    expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
    await Worker.waitForWorker()
  })
1

There are 1 answers

0
Brenden On BEST ANSWER

I think there are a couple things you can do here.

Inject status handlers

You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.

function waitForWorker(onComplete, onBusy) {
   // instead of calling waitForWorker call onBusy.
   // instead of calling workerComplete call onComplete.
}

Now to test, you really just need to create mock functions.

const onComplete = jest.fn();
const onBusy = jest.fn();

And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.

Return a promise

You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.

async function waitForWorker() {
  let result = { status: 'empty' };

  if (!$('.worker-waiter').length) {
    return result;
  }
  
  try {
    const working_statuses = ['queued', 'working'];
    const data = await checkWorkerStatus();

    if (data && working_statuses.includes(data.status)) {
      await waitForWorker();
    } else {
      result = { status: 'complete' };
    }
  } catch (e) {
    result = { status: 'error' };
  }

  return result;
}

The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.

If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.

async function waitForWorker(request) {
 // ...

  try {
    const working_statuses = ['queued', 'working'];
    const data = await request();
  } catch (e) {
    // ...
  }
  
  // ...
}

You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.

  const result = await waitForWorker(checkWorkerStatus);
  
  if (result.status === 'complete') {
    workerComplete();
  }