2

I'm attempting to write a data-fetching "hook" (not entirely clear what the word for this is in Vue, but a state function that does not render a template). The Hook takes an asynchronous data resolver as a parameter. The hook itself is very simple, it simply adds loading state to a function that returns a promise.

import { ref, watch } from "vue";

export function useDataFetcher<T>(
  resolver: () => Promise<T>,
) {
  const isLoading = ref(false);
  const data = ref<T>();
  watch(resolver, () => {
    isLoading.value = true;
    resolver(...parameters)
      .then((fetchedData) => {
        data.value = fetchedData;
      })
      .finally(() => {
        isLoading.value = false;
      });
  });
  return {
    isLoading: isLoading.value,
    data,
    parameters,
  };
}

I am attempting to write a test against this function to ensure that the isLoading method is updating correctly:

import { useDataFetcher } from "./useDataFetcher";
test("While the fetcher is loading data, isLoading should be true", async () => {
  const promise = new Promise<void>((resolver) =>
    setTimeout(() => resolver(), 2000)
  );
  const { isLoading } = useDataFetcher(() => promise);
  expect(isLoading).toBeTruthy();
  await promise;

  expect(isLoading).toBeFalsy();
});

As written, this test is not working. I have not seen a lot of information in the interwebs about testing these state functions in Vue. There are two stack overflow questions that seem related: Is it possible to test a Vue 3 Composition library which uses inject without a wrapping component?

and How to unit test standalone Vue composition

But neither of these seem to quite scratch the itch I'm having here.

In React, you have the @testing-library/react-hooks library to manage these tests, and it makes it very simple. It seems to me that I'm missing something to the effect of await Vue.nextTick().

So, finally, the question: What exactly is the best way to test these Vue hooks that don't render templates?

2 Answers 2

1

So, I ended up putting together a solution for my problem and publishing an npm module for it: https://www.npmjs.com/package/vue-composable-function-tester. I would love feedback on the solution.

Here's an example of what it looks like:

Test:

it("Reacts to a resolving promise", async () => {
  const resolvedData = {
    hello: "world",
  };
  const promise = Promise.resolve(resolvedData);
  const composable = mountComposableFunction(() =>
    useAsynchronousLoader(() => promise)
  );
  await composable.nextTick();
  expect(composable.data.isLoading.value).toBe(true);
  await promise;
  await composable.nextTick();
  expect(composable.data.data.value).toStrictEqual(resolvedData);
  await composable.nextTick();
  expect(composable.data.isLoading.value).toBe(false);
});

Implementation:

export function useAsynchronousLoader<T>(promiseCreator: () => Promise<T>) {
  const isLoading = ref(false);
  const data = ref<T>();
  const error = ref<object>();
  isLoading.value = true;
  promiseCreator()
    .then((newData) => {
      data.value = newData;
    })
    .catch((e) => {
      error.value = e;
    })
    .finally(() => {
      isLoading.value = false;
    });
  return {
    isLoading,
    data,
    error,
  };
}

EDIT: Improved code samples.

Sign up to request clarification or add additional context in comments.

Comments

0

You need to return the loading ref to keep the reactivity.

  return {
    isLoading,
    data,
    parameters,
  };

By passing isLoading.value you only pass the value at that time and loose the reactivity

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.