【问题标题】:Unit test if event handler called promises one after the other (synchronously)单元测试事件处理程序是否一个接一个地调用承诺(同步)
【发布时间】:2020-05-22 09:38:47
【问题描述】:

我有一个 Vue 按钮单击处理程序,它基于它所接受的参数,可以:

  • 只调用请求A
  • 只调用请求 B
  • 调用请求A和B - 但它应该一个接一个地调用它们(如果请求A成功返回,则调用请求B。基本上,实现不能使用Promise.all())。

我的问题是我不知道如何使用 Jest 对“依次调用 A 和 B”行为进行单元测试。

实施

这是事件处理程序,它在单击按钮后运行:

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
  }
}

测试

正确的测试用例应该:

  • 失败❌当:

    • 实现同时调用两个请求,例如:

      • () => { Promise.all([loadA(), loadB()]) }
      • () => { loadA(); loadB() }
  • 通过✔️何时:

    • 实现调用 loadA,等待它的 promise 解决,然后调用 loadB,例如:
      • () => {await loadA(); await loadB();}

这是我对我描述的测试用例的看法,但它似乎很容易受到竞争条件的影响,而且我假设的同事很难理解。

//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
}
}

有没有更好的方法来测试这种行为?我知道例如jest.advanceTimersByTime,但这会提前所有计时器,而不是当前计时器。

【问题讨论】:

    标签: javascript unit-testing vue.js jestjs vue-test-utils


    【解决方案1】:

    我会用一些处理程序替换await new Promise(r => setTimeout(r, 500));

    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
      }
    }
    

    返回承诺。

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

    **示例 sn-p **

    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>

    【讨论】:

    • 感谢您的回复。我看到您将断言更改为expect(callCount).toBe(2),但我认为运行您的测试用例会产生误报。等待 this.handler 将导致测试用例在 loadA 和 loadB 都解决后继续执行 - 此时无法判断它们是一个接一个调用还是同时调用。我为我的问题添加了更多描述
    • 实际上很难测试这样的事情,因为您无法跟踪函数内部执行承诺的顺序。潜在的解决方法是async generator function,如果每次迭代都调用所需的函数,您可以单独测试它。它的示例在 sn-p 中。不过我不会推荐它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-07
    • 1970-01-01
    • 2018-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多