Vitest で localStorage の単体テストを実装する

# はじめに
この記事では、Vitest というテストフレームワークで localStorage がどのように動作をするか確認していきます。

https://vitest.dev/

https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage

# 開発環境
開発環境は以下の通りです。
- Windows11
- VSCode
- TypeScript 4.9.5
- React 18.2.0
- Vite 4.1.5
- Vitest 0.34.5
- jsdom 22.1.0

# テスト対象コードの実装
テストを実装する前に localStorage を利用したテスト対象となるコードを実装します。

```localStorage.ts
export const BOOK_IN_RENTAL_KEY = "book-in-rental";

export const getBookInRental = () => localStorage.getItem(BOOK_IN_RENTAL_KEY);

export const setBookInRental = (book: string) =>
  localStorage.setItem(BOOK_IN_RENTAL_KEY, book);
```

今回はこちらの ```getBookInRental``` と ```setBookInRental``` の単体テストの実装を通して、localStorage の動作について確認していきます。

# 値のテスト
まずは、シンプルに ```setBookInRental``` で設定した値を ```getBookInRental``` で取得できるかテストします。

```localStorage.test.ts
describe("localStorage tests", () => {
  test("get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    setBookInRental(DUMMY_BOOK_IN_RENTAL);

    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
  });
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/007acb5a-39bc-d22c-9a73-c03c52f0c881.png)

次は、複数のテスト間での localStorage の動作を確認します。

```localStorage.test.ts
describe("localStorage tests", () => {
  test("get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    setBookInRental(DUMMY_BOOK_IN_RENTAL);

    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
  });
  test("get a book in rental from localStorage without setting it", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
  });
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/ac178bc2-961a-49f6-b56a-097e1e19ab5e.png)

上記の2つ目のテスト（```"get a book in rental from localStorage without setting it"```）では、値を設定していませんが、1つ目のテスト（```"get a book in rental from localStorage"```）で設定した値を取得できることが確認できます。

また、以下のようにテストごとに ```localStorage.clear()``` すれば、テスト間で値が共有されることを防ぐことができます（とあるユニットテストのデータが別のユニットテストのデータに影響を及ぼすことを防止できます）。

```localStorage.test.ts
describe("localStorage tests", () => {
  afterEach(() => {
    localStorage.clear();
  });
  test("get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    setBookInRental(DUMMY_BOOK_IN_RENTAL);

    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
  });
  test("not get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    expect(getBookInRental()).not.toBe(DUMMY_BOOK_IN_RENTAL);
  });
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/261be9e6-ff8e-5918-45db-c75003919c3a.png)


# 関数が呼び出されていることのテスト
```vi.spyOn``` を利用して ```getItem``` や ```setItem``` が呼び出されているかどうか確認します。
なお、仮想DOMとして jsdom を利用する場合、localStorage を spy on する際は、localStorage ではなく、```Storage.prototype``` を第一引数に渡す必要があるようです。

まずは ```getItem``` が呼び出されているか確認します。

```localStorage.test.ts
describe("localStorage tests", () => {
  const getItemSpy = vi.spyOn(Storage.prototype, "getItem");

  afterEach(() => {
    getItemSpy.mockClear();
    localStorage.clear();
  });
  test("get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    setBookInRental(DUMMY_BOOK_IN_RENTAL);

    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
    expect(getItemSpy).toHaveBeenCalledWith(BOOK_IN_RENTAL_KEY);
  });
  test("not get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    expect(getBookInRental()).not.toBe(DUMMY_BOOK_IN_RENTAL);
    expect(getItemSpy).toHaveBeenCalledWith(BOOK_IN_RENTAL_KEY);
  });
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/fb308bc5-3b28-6eb9-2cad-91c067a85134.png)

```setItem``` も同じように実装して、呼び出されているかどうか確認します。

```localStorage.test.ts
describe("localStorage tests", () => {
  const getItemSpy = vi.spyOn(Storage.prototype, "getItem");
  const setItemSpy = vi.spyOn(Storage.prototype, "setItem");

  afterEach(() => {
    getItemSpy.mockClear();
    setItemSpy.mockClear();
    localStorage.clear();
  });
  test("set and get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";

    setBookInRental(DUMMY_BOOK_IN_RENTAL);
    expect(setItemSpy).toHaveBeenCalledWith(
      BOOK_IN_RENTAL_KEY,
      DUMMY_BOOK_IN_RENTAL
    );

    expect(getBookInRental()).toBe(DUMMY_BOOK_IN_RENTAL);
    expect(getItemSpy).toHaveBeenCalledWith(BOOK_IN_RENTAL_KEY);
  });
  test("not get a book in rental from localStorage", () => {
    const DUMMY_BOOK_IN_RENTAL = "test book";
    expect(getBookInRental()).not.toBe(DUMMY_BOOK_IN_RENTAL);
    expect(getItemSpy).toHaveBeenCalledWith(BOOK_IN_RENTAL_KEY);
  });
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/89810a9e-585c-f03d-efcc-f3481ec40f8f.png)

# まとめ
今回は localStorage を利用した値の入出力及び関数呼び出しの単体テストの動作を確認しました。今後は localStorage を利用した結合テストや localStorage を利用したライブラリを利用したコードのテストの動作確認もしてみたいです。

# 参考
- [How to Test LocalStorage with Vitest](https://runthatline.com/vitest-mock-localstorage/)
- [Test API Referenece](https://vitest.dev/api/)