I have a Vue button click handler that based on the arguments it takes, can:
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.
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
}
}
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:
() => {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.