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

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

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

# 前提条件

- Vue 3.3.4
- Vitest 0.34.5

# 結論

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

# 実例

[Vueのドキュメント中にリンクが張ってある](https://play.vuejs.org/#eNp9Vdtu20YQ/ZUpUUA0qpAOjL4YktCbC7Rom8BN8sSHrMihtfZql9iLZEHgv2dml6SpxMiDIWkuZ+acmR2fs1+7rjgEzG6zlaut7Dw49KHbVFruO2M9nMFiu4Ta7LvgsYEeWmv2sKCkxSwoOPwTfb2b/EU5mopHR5GVro12HrbC4UerYA2Lnfeduy3LR2d0p0SNO6MatIU/dbI2DRZUtPSmMa4kgJQuG8qkjvLF28XVaAwRb2wxz69gvZkK/UQ5xUGogBQ/ZpyhEV4sAa01lnpeTwRyApsFWvT2RO6Eea40THBMgfq6NLwlS1/pVZnUJB3ph8c98fNIvwD+MaKBzkQut2xYbYP3RsPhTWvsusokSA0/Vxn8UitZP7GFSX/+8Sz7z1W2OZ9BQt+vypQXS1R+1cgDQciW4iMrimR0wu8270znfoC7SBaJWdAeLTa3QFgxuNijc+IBIy5PPyYOjU19RDEI954/Z/UptKTy6VvqA5XD1AwLTTl/0Aco4s5lV51F5sG+VJJ+v4qxYbmkfiiKYvSvyknPbJnNtoyW+HJpj4Icd22LtV+CN5/ikC4XuNL4HFPaoGsvie3FIqSJp1WIzabl00HxkoyetEVfufhv1kAu3EnX8z0CKEtKofcGzhMb2CItAELL1SPlFMV1pwVj+GROc/vWPoc26oDgdxhfSArlLnbWaBOcOoEzIP3CgbeifqLXLRyICaDBDnVD+3KC7emCSyQ4sifspOx61Hh4Qy/d8BsaOEdkYb1sZS2FoiJKnIC6FbqhsaTVZfk8gDgK6cHLPZowFGUzAQTNWl/BUSrFbzRYHXmSdeAp28RMsI0fyFDaUJg9Spd0SbERZcvZDBRleCPdQMCPh8ARwdRRnBCTjGz5WkT0i0GlSMqixTR6VKyHmmWEHIfV+naSOETyRx8vEYwMv7pa8dJU+hU9Kz2t86ReqjcgaTzCe3oGpEOeD4uyJOcjTXe+obScHwaAi82lo9dC/q/wuyINjrwbuC5uZrS4WAQeyTN9ftOXIVwy537iecoX92kR4q/F1UvqIMsSbq6vo5XF6ekCeEcTauVDFJpuQESvMv53IBXadx3r4KqMrt0w0kwoZY5/R5u3AZejvd5h/fSK/dE9s63K3vN7tQesssnnhX1An9x3//+Hz/R9cu5NExRFf8d5zyIF7jGF/RZ0Q23P4mK3f8XLRmfhg7t79qjdSIobjXLE+Cqju/b7d6i/tHtT3MQ8VrH/Ahstp5A=) リアクティブな 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` を追加すれば良い。

```diff_typescript
  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 };
}
```

```diff_typescript
-   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があった。

https://vitest.dev/api/vi.html#vi-waitfor-0-34-5

これを使えばもっとスマートにかけた。

```diff_typescript
-   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` で大丈夫ぽいです。

```diff_typescript
  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" });
```