t3-stack入門 (5) t3-appにVitestを導入する

t3-appにテスト関連の設定は一切書かれていません。潔いですね。
急いでUnitTest用のフレームワークを導入することとします。

## 何を導入するか
テストを導入するとすると第一選択肢はJestなんでしょうが、
trpcのexamples-next-prisma-starterを見てみるとより高速なVitestが使われているようです。
Testは早いに越したことはありません。
Jestもesbuil等を使えば早くなるみたいですが、とりあえずexamplesで使われているVitestを入れてみます。

https://github.com/trpc/examples-next-prisma-starter

## vitestの設定
Vitestをインストール。
```
~/t3-app-example$ npm install -D vite vitest
```
vitest.config.tsを追加します。とりあえずexamplesのものをそのままコピー
```typescript
import { fileURLToPath } from "url";
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    exclude: [...configDefaults.exclude, "**/playwright/**"],
    alias: {
      "~/": fileURLToPath(new URL("./src/", import.meta.url)),
    },
  },
});
```

## testの作成
src/server/trpc/router/todo.test.tsにテストを書きます。とりあえずダミーのテストを追加。

```typescript
import { describe, expect, it } from "vitest";
function add(lhs: number, rhs: number) {
  return lhs + rhs;
}

describe("add", () => {
  it("1 + 2 = 3", () => {
    const result = add(1, 2);

    expect(result).toBe(3);
  });
});
```
vitestを実行
```shell-session
~/t3-app-example$ npx vitest

 DEV  v0.25.3 /home/uehara/t3-app-example

 ✓ src/server/trpc/router/todo.test.ts (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  22:53:34
   Duration  966ms (transform 528ms, setup 0ms, collect 24ms, tests 4ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit
```

テスト実行まではできているようなので、todoRouterのテストを作成します。

```typescript
import { test, expect } from "vitest";

import { createContextInner } from "../context";
import type { AppRouter } from "./_app";
import { appRouter } from "./_app";
import type { inferProcedureInput } from "@trpc/server";

test("create and getAll todos", async () => {
  const ctx = await createContextInner({
    session: {
      user: {
        id: "id12345",
        name: "testUser",
        email: "test@test.com",
      },
      expires: "1234567",
    },
  });
  const caller = appRouter.createCaller(ctx);

  const input: inferProcedureInput<AppRouter["todo"]["create"]> = {
    title: "Add Unit test",
    description: "Add Unit test by vitest",
  };

  const post = await caller.todo.create(input);
  const posts = await caller.todo.getAll();
  expect(posts.length).toBe(1);
  expect(post.id).toBeGreaterThan(0);
});
```
が、今のままだとテストが実行されるたびにdbにTodoが追加されるので、複数回実行すると当然エラーになります。
```shell-session
~/t3-todo-example$ npx vitest

 DEV  v0.25.8 /home/uehara/t3-todo-example

 ❯ src/server/trpc/router/todo.test.ts (1)
   × create and getAll todos

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/server/trpc/router/todo.test.ts > create and getAll todos
AssertionError: expected 2 to be 1 // Object.is equality
 ❯ src/server/trpc/router/todo.test.ts:28:24
     26|   const post = await caller.todo.create(input);
     27|   const posts = await caller.todo.getAll();
     28|   expect(posts.length).toBe(1);
       |                        ^
     29|   expect(post.id).toBeGreaterThan(0);
     30| });

  - Expected   "1"
  + Received   "2"

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  15:42:51
   Duration  1.54s (transform 652ms, setup 0ms, collect 454ms, tests 56ms)


 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit
```

## テスト用データベースのリセット方法
どうしようか迷ったんですが、こちらの記事を参考にworkerごとに接続先を変える。だたし今使っているのはsqliteなので、データベースのresetは```npx prisma migrate reset```を使うのではなく、dbファイルをコピーすることにしました。

https://www.mizdra.net/entry/2022/11/24/153459

vitest.config.tsにvitest.setup.tsを読み込むように設定します。
```diff_typescript
    },
+    setupFiles: "./vitest.setup.ts",
  },
});
```

vitest.setup.tsでworkerごとに接続先を変え、その接続先のファイルに、resetされたデータベースファイルをコピーすることにします。
 
```typescript
import process from "process";
import fs from "fs";
import { beforeAll, afterAll } from "vitest";

// DATABASE_URL には 'mysql://user:password@localhost:3306/db' のような文字列が入っている想定。
process.env.DATABASE_URL = `file:./db.sqlite-test-${process.env.VITEST_WORKER_ID}`;

beforeAll(async () => {
  await fs.copyFileSync(
    "prisma/reset.sqlite",
    `prisma/db.sqlite-test-${process.env.VITEST_WORKER_ID}`
  );
});
afterAll(async () => {
  fs.unlinkSync(`prisma/db.sqlite-test-${process.env.VITEST_WORKER_ID}`);
});
```
これで、Workerごとにresetされたデータベースが用意されるようになりました。
sqlite以外を使う場合は```npx prisma migrate reset```を使うしかないのでしょうか。
postgresql,mysqlもsqliteみたいに1つのファイルでデータベースが起動するモードを
作ってくれると楽なんですが。

```shell-session
~/t3-app-example$ npx vitest

 DEV  v0.25.3 /home/uehara/t3-app-example

stdout | src/server/trpc/router/todo.test.ts > create and getAll todos
1

 ✓ src/server/trpc/router/todo.test.ts (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  23:51:58
   Duration  1.48s (transform 661ms, setup 21ms, collect 362ms, tests 51ms)


 PASS  Waiting for file changes...
       press h to show help, press q to quit
```
ここまでの変更は以下の通りです。

https://github.com/bioflowy/t3-todo-example/commit/11234325b1119dcfabdb7d66ed4ea7022a83348f