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 };
}
この場合、 data
か error
に値が入るのを待つしかない。
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
が更新された際にリアクティブに動作することを確認するときは、 nextTick
と waitFor
を組み合わせる。
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" });