LoginSignup
1
2

watchEffect で非同期処理をしている場合のテスト

Last updated at Posted at 2024-02-07

React から Vue に戻ってきたので、メモ。

前にも非同期処理のテストで苦戦していたのを思い出します。
https://qiita.com/akagire/items/c4b2bdfe51bf4971dd8c

前提条件

  • Vue 3.3.4
  • Vitest 0.34.5

結論

非同期処理が終わるのを待つしかない。。

実例

Vueのドキュメント中にリンクが張ってある リアクティブな composables のサンプルを例に。

useFetch.ts
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 };
}

この場合、 dataerror に値が入るのを待つしかない。

useFetch.spec.ts
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));

てことで、改めて完成形

useFetch.spec.ts
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 が更新された際にリアクティブに動作することを確認するときは、 nextTickwaitFor を組み合わせる。

useFetch.spec.ts
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" });
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2