简体   繁体   中英

Unit test if event handler called promises one after the other (synchronously)

I have a Vue button click handler that based on the arguments it takes, can:

  • call just request A
  • call just request B
  • call request A and B - but it should call them one after the other (if request A returns successfully, then call request B. Basically, the implementation can't use Promise.all() ).

My problem is that I don't know how to unit test the "call A and B one after the other" behavior with Jest.

Implementation

Here's the event handler, it runs after clicking a button:

const loadA = this.$store.dispatch['loadA']; //note these are FUNCTIONS THAT RETURNS A PROMISE
const loadB = this.$store.dispatch['loadB'];
async makeRequests(shouldMakeRequestA, shouldMakeRequestB) {
  const requests = [];
  if(shouldMakeRequestA) requests.push(loadA);
  if(shouldMakeRequestB) requests.push(loadB);

  for(const request in requests) {
    await request(); //waits for request to return before calling for second one
  }
}

Testing

The correct test case should:

  • FAIL ❌ when:

    • the implementation calls two requests at the same time eg.:

      • () => { Promise.all([loadA(), loadB()]) }
      • () => { loadA(); loadB() }
  • PASS ✔️ when:

    • the implementation calls loadA, waits for it's promise to resolve, then calls loadB, eg:
      • () => {await loadA(); await loadB();}

Here's my take on the test case I described, but it seems very fragile to race conditions and would be difficult to understand to colleagues I assume.

//component.spec.js
import MyCustomButton from '@/components/MyCustomButton.vue'
import TheComponentWeAreTesting from '@/components/TheComponentWeAreTesting'
describe('foo', () => {
const resolveAfterOneSecond = () => new Promise(r => setTimeout(r, 1000));

let wrapper;
const loadA = jest.fn(resolveAfterOneSecond);
const loadB = jest.fn(resolveAfterOneSecond);

beforeEach(() => {
  wrapper = shallowMount(TheComponentWeAreTesting, store: new Vuex.Store({actions: {loadA, loadB}});
})

it('runs A and B one after the other', async () => {
  wrapper.find(MyCustomButton).vm.$emit('click');
  /*
    One of the major problems with my approach is that
    I don't know how much time has passed after I await $nextTick.
    Both requests resolve after 2000 ms total (as mocked above with setTimeout)
    But how much time has passed after $nextTick is resolved?
    700ms? 1300? 1999ms?
  */
  await wrapper.vm.$nextTick();
  /*
   Because I don't know how much time did it take for $nextTick to resolve
   I need to wait a few extra ms so the test passes at all
   Basically, you have to take my word for it that "500ms" is the value that makes the test pass
  */
  awat new Promise(r => setTimeout(r, 500));
  const callCount = loadA.mock.calls.length + loadB.mock.calls.length;
  expect(callCount).toBe(1); //expect first request to have been sent out, but the second one shouldn't be sent out yet at this point
}
}

Is there a better way to test this behavior? I know of eg jest.advanceTimersByTime , but that advances ALL timers, not the current one.

I would replace await new Promise(r => setTimeout(r, 500)); with some handler as

async makeRequests(shouldMakeRequestA, shouldMakeRequestB) {
  const requests = [];
  if(shouldMakeRequestA) requests.push(loadA);
  if(shouldMakeRequestB) requests.push(loadB);

  for(const request in requests) {
    await request(); //waits for request to return before calling for second one
  }
}

returns promise.

this.handler = (async() => {
        const requests = [];
        if (shouldMakeRequestA) requests.push(loadA);
        if (shouldMakeRequestB) requests.push(loadB);

        for (const request of requests) {
          await request();
        }
      })()

**Example snippet **

 Vue.config.devtools = false; Vue.config.productionTip = false; const { shallowMount } = VueTestUtils; const { core: { beforeEach, describe, it, expect, run, jest }, } = window.jestLite; const resolveAfterOneSecond = () => new Promise(r => setTimeout(r, 1000)); let loadA = resolveAfterOneSecond; let loadB = resolveAfterOneSecond; const combineAndSendRequests = async function*(shouldMakeRequestA, shouldMakeRequestB) { if (shouldMakeRequestA) { await loadA(); yield 1; } if (shouldMakeRequestB) { await loadB(); yield 2; } } const TestComponent = Vue.component('test-component', { template: `<button @click="sendRequests()">Send</button>`, data() { return { handler: null } }, methods: { sendRequests() { const shouldMakeRequestA = true; const shouldMakeRequestB = true; this.handler = (async() => { for await (let promise of combineAndSendRequests(shouldMakeRequestA, shouldMakeRequestB)) { } })(); } } }) var app = new Vue({ el: '#app' }) document.querySelector("#tests").addEventListener("click", (event) => { const element = event.target; element.dataset.running = true; element.textContent = "Running..." loadA = jest.fn(resolveAfterOneSecond); loadB = jest.fn(resolveAfterOneSecond); describe("combineAndSendRequests", () => { it('runs A and B one after the other', async() => { const shouldMakeRequestA = true; const shouldMakeRequestB = true; const iterator = combineAndSendRequests(shouldMakeRequestA, shouldMakeRequestB); await iterator.next(); let loadACallsCount = loadA.mock.calls.length; let loadBCallsCount = loadB.mock.calls.length; expect(loadACallsCount).toBe(1); expect(loadBCallsCount).toBe(0); await iterator.next(); loadBCallsCount = loadB.mock.calls.length; expect(loadBCallsCount).toBe(1); const callsCount = loadA.mock.calls.length + loadB.mock.calls.length; expect(callsCount).toBe(2); }); }); describe("test-component", () => { let wrapper = null; beforeEach(() => { wrapper = shallowMount(TestComponent); }) it('runs request after click', async() => { wrapper.find("button").trigger('click'); await wrapper.vm.$nextTick(); const handler = wrapper.vm.$data.handler; expect(handler).not.toBe(null); }); }); run().then(result => { console.log(result); delete element.dataset.running; if (.result.some(pr => pr.status.includes("fail"))) { element.textContent = "Passed." element;dataset.pass = true. } else { element.textContent = "Fail;" element.dataset.fail = true; } }) })
 #tests { margin-top: 1rem; padding: 0.5rem; border: 1px solid black; cursor: pointer; } button[data-pass] { background: green; color: white; } button[data-running] { background: orange; color: white; } button[data-fail] { background: red; color: white; }
 <script src="https://unpkg.com/jest-lite@1.0.0-alpha.4/dist/core.js"></script> <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script> <script src="https://www.unpkg.com/vue-template-compiler@2.6.11/browser.js"></script> <script src="https://unpkg.com/@vue/test-utils@1.0.3/dist/vue-test-utils.umd.js"></script> <div id="app"> <test-component></test-component> </div> <button id="tests">Run Tests</button>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM