React から Vue に戻ってきたので、メモ。
前にも非同期処理のテストで苦戦していたのを思い出します。
https://qiita.com/akagire/items/c4b2bdfe51bf4971dd8c
前提条件
- Vue 3.3.4
- Vitest 0.34.5
結論
非同期処理が終わるのを待つしかない。。
実例
Vueのドキュメント中にリンクが張ってある リアクティブな composables のサンプルを例に。
import { ref, watchEffect, toValue } from "vue";
export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  watchEffect(async () => {
    data.value = null;
    error.value = null;
    const urlValue = toValue(url);
    try {
      const res = await fetch(urlValue);
      data.value = await res.json();
    } catch (e) {
      error.value = e;
    }
  });
  return { data, error };
}
この場合、 data か error に値が入るのを待つしかない。
import { describe, test, beforeEach, vi, expect } from "vitest";
import { ref } from "vue";
import { useFetch } from "./useFetch";
test("fetch 結果が得られる", async () => {
  vi.spyOn(global, "fetch").mockResolvedValue(
    new Response('{"message":"hello"}'),
  );
  const endpointRef = ref("http://example.com");
  const { data, error } = useFetch(endpointRef);
  while (data !== null || error !== null) {
    await new Promise((resolve) => setTimeout(resolve, 1));
  }
  expect(error.value).toBeNull();
  expect(address.value).toStrictEqual({ message: "hello" });
});
この while 部分がイケてないので、React でよく使う isLoading を追加すれば良い。
  import { ref, watchEffect, toValue } from "vue";
  export function useFetch(url) {
    const data = ref(null);
    const error = ref(null);
+   const isLoading = ref(false);
    watchEffect(async () => {
      data.value = null;
      error.value = null;
+     isLoading.value = true;
      const urlValue = toValue(url);
      try {
        const res = await fetch(urlValue);
        data.value = await res.json();
      } catch (e) {
        error.value = e;
      }
+     isLoading.value = false;
  });
-  return { data, error };
+  return { data, error, isLoading };
}
-   const { data, error } = useFetch(endpointRef);
+   const { data, error, isLoading } = useFetch(endpointRef);
-   while (data !== null || error !== null) {
+   while (!isLoading)
      await new Promise((resolve) => setTimeout(resolve, 1));
    }
あとは、 while も辞めたい。。ので、 vitest のドキュメント眺めたら waitFor というAPIがあった。
これを使えばもっとスマートにかけた。
-   while (!isLoading)
-      await new Promise((resolve) => setTimeout(resolve, 1));
-   }
+   await vi.waitFor(() => expect(isLoading.value).toBe(false));
てことで、改めて完成形
import { describe, test, beforeEach, vi, expect } from "vitest";
import { ref } from "vue";
import { useFetch } from "./useFetch";
test("fetch 結果が得られる", async () => {
  vi.spyOn(global, "fetch").mockResolvedValue(
    new Response('{"message":"hello"}'),
  );
  const endpointRef = ref("http://example.com");
  const { data, error, isLoading } = useFetch(endpointRef);
  await vi.waitFor(() => expect(isLoading.value).toBe(false));
  expect(error.value).toBeNull();
  expect(address.value).toStrictEqual({ message: "hello" });
});
感想
React Test Library の act 相当の API があれば良いんですが...
もっといい方法があれば教えてください。
余談
watchEffect の中身が非同期処理ではないならば、 nextTick で大丈夫ぽいです。
  import { describe, test, beforeEach, vi, expect } from "vitest";
- import { ref } from "vue";
+ import { ref, nextTick } from "vue";
// 略
  const { data, error, isLoading } = useXXX(xxxRef);
+ await nextTick();
  expect(data.value).toBe(...);
更に余談 (追記)
この useFetch はリアクティブなので、 endpointRef が更新された際にリアクティブに動作することを確認するときは、 nextTick と waitFor を組み合わせる。
encpointRef.value = "http://example.com/v2/hoge";
await nextTick();
await vi.waitFor(() => expect(isLoading.value).toBe(false));
expect(error.value).toBeNull();
expect(address.value).toStrictEqual({ message: "hello" });