🔲

TypeScriptとpnpmとdockerで自動テストを導入した話

2025/01/17に公開

環境

  • バックエンドフレームワーク: hono
  • フロントフレームワーク: Next.js
  • ORM: prisma
  • ランタイム: Node.js
  • テスト: vitest
  • dockerランタイム: PodmanDesktop(ファイルシステムにvirtiofsを使用)
  • PC: MacBookPro M2
  • DB: TiDB
  • CI: Github Actions

2023年にPHPでテストを導入したときの反省点

元々テストが無いコードだったのでユニットテストが書きづらく、インテグレーションテストメインでテストを記述するように導入した。

チームの方々はテスト導入後に挫折せずにテストを書き続けてくれてとてもよかった。

がしかし、インテグレーションテストだけ増えてしまいテストの実行時間が課題になった。

テストが増えれば実行時間が掛かるが、インテグレーションテストという重めのテストばかり増えると、テスト実行時間の問題に1年半ぐらいで到達するという結果に。

ただ、モックだらけのユニットテストにすると信頼性が無くなるので、全部ユニットテストというのもどうかな?と思う。

t_wadaさんも、「ローカルでできることは全部やる」と言っていた。

なので、テストの大部分をユニットテストで記述して、インテグレーションテストは正常系とエラー系を一本ずつ通すぐらいとしておけば、よかったかな?と今では思う。

今回のコンセプト:実行速度大事!!

色々な現場でテストが増えてくると実行時間が課題となってくる。

開発初期からテストを導入できるなら、改善できることも多いはずなので頑張りたい!!

テストランナーにvitestを採用

vite関連のツールだが、viteじゃなくても使えるので実行も速いし良いと思う。

前の現場ではJestを使っていたが、テストが増えてくると重くなったり、動作が不安定になったりしたので採用を見送った。

honoは立ち上げなくてもインテグレーションテストが書けるという

import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'

// docker exec -t hoge sh -c 'pnpm exec vitest run ./tests/unit/example.test.ts'

const app = new Hono()
app.get('/', (c) => {
  return c.json([{ id: 1, title: 'title1' }])
})
app.post('/posts', async (c) => {
  const { title } = await c.req.json()
  return c.json({ message: 'Created', title }, 201)
})

describe('example test', () => {
  it('GET / should return a list of posts', async () => {
    const res = await app.request('/')
    expect(res.status).toEqual(200)
    expect(await res.json()).toEqual([{ id: 1, title: 'title1' }])
  })

  it('POST /posts should create a new post', async () => {
    const res = await app.request('/posts', {
      method: 'POST',
      body: JSON.stringify({ title: 'New Post' }),
      headers: { 'Content-Type': 'application/json' },
    })
    expect(res.status).toEqual(201)
    expect(await res.json()).toEqual({ message: 'Created', title: 'New Post' })
  })
})

こんなシンプルに立ち上げもせずにテストを書けるとは。流行るわけだ。

ユニットテストをメインにしたテスト実装にしたい

できれば単なる関数を増やせると良いと思っている。

7割ぐらいまでなら単なる関数でいけるのでは?という話はある。

(昔staticおじさんと馬鹿にする風潮があったがそれは気にしない)

DIコンテナ使わないと作成できないオブジェクトをテストしたいとなると、テスト前にコンテナ稼働させる必要があり、速度に影響が出るから出来ればそうはなってほしくないところ。

GithubActionsのCI上での工夫

全員M1以降のMacを使っているのでローカルから本番までCPUのアーキテクチャはARMで統一

AWSのGravitonCPUは、Webシステムでは速度も最高らしい。
コストも価格/性能比で見ても良い。

ローカル開発時はdocker composeでappコンテナを使っているが、CIではappを外して起動する方式とした

# appは起動させずにtidbだけ起動するようにしている
# また、ランナーをARM64のCPUにしたが、amd64と誤認識されてしまうためplatformを指定して起動する
DOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose up -d tidb

tidbのdockerイメージのキャッシュ

tidbは下記のDockerfileを使っているので、dockerイメージも時短のためキャッシュするようにしている。
https://github.com/ti-click/docker-tidb-playground

Node.jsのセットアップや実行はCI側で行う

Node.jsは、さすがの知名度で簡単にセットアップできるので、CI上でテスト実行することにした。
(仮想環境内からは実行しないこととした。)

余談:オブジェクトよりも単なる関数

単なる関数は簡単に呼び出せるし引数だけ気にすれば良い。

オブジェクト指向が良いと言われているが、テストだけを見るなら単なる関数のほうが書きやすい。

また、最近ではデータと処理が一体になっているオブジェクト指向はデータと処理に分離した方が良いのでは?というトレンドもあるみたいで、そっちが主流になってほしいところ。

リッチ・ヒッキー派なので、シンプルさを極めたいところ。

一般的ではない?から実現していないが、いつか下記の記事の内容を中心としたチーム開発をしてみたいと思っている。
https://eed3si9n.com/ja/simplicity-matters/

ハマったところ

Macでdocker環境内からpnpmを使うとエラーが出た

【対応】
https://zenn.dev/daijinload/articles/eecae9c466b85c

AI検索をメインで使っていたが普通にググった方が速いときもあった

しかも、出てきた記事が上記の自分が書いたものというダブルパンチでもあった(完全に忘れてた)

【対応】
たまにはググろう

npmライブラリがネイティブバイナリを使うため、MacとLinux環境で切り替えるとエラーが出る

JavaScriptだけなら特にエラーはでないけど、最近は速度重視のためにRustなどのバイナリを使っているから、OSごとにセットアップが必要っぽい

【対応】
切り替える必要がある場合、一旦クリーンしてから、pnpm i することにした

開発用のDockerfileが無かった

これはテスト導入とは関係ない話。

Next.jsのbuildしてstartする本番用しかなかったので、docker composeなどで起動してコードの変更を即反映できなかった。

【対応】
Next.jsがdevモードで起動する開発用Dockerfileを作成した。

node_modulesのセットアップが無いとdocker composeで起動できない

上述のLinuxとMacの切り替えをするには、コンテナ内から pnpm i をしたいところ。

だがしかし、node_moduelsを削除しちゃうと、docker composeではappが起動しないため実行できない。

【対応】
node_modulesだけ入れば良いので、公開イメージを使ったRunコマンドで対応

# Linux内からだと消せなかったりしたので、Mac側で一旦消す
rm -rf .pnpm-store/ node_modules/ && pnpm store prune

# 公開イメージを使ってインストールする
docker run --env-file .env --rm -w /app -v $(pwd):/app node:22-alpine sh -c 'npm i -g pnpm && pnpm config set store-dir /tmp/pnpm/store && pnpm i'

余談だがインストールコマンドには、上述のdocker環境内での pnpm i エラー回避用のstore設定とか、 .env を読み込みが含まれている。

GithubActionsでインテグレーションテストのCIを動かすと時間が掛かる

dockerを動かす関係上、imageの作成に時間が掛かっていた

【対応】
Dockerfileに変更があったときだけimage作成して、通常時はキャッシュを使うことにした

感想

本当はインテグレーションテストが完璧な動作なので、それを軽く実行できるようにRustなどでWebシステムを作ると良いのかな?と思ったりします。

あとは、ORMが無駄なデータ引っ張ってくるから最小限で済むようにする実装にしたりとか。

システム自体のパフォーマンスを考えて実装することが、テスト時間の短縮にも繋がり美味しい状況になるなと思います。

それと、去年PHPでテスト導入した知見がだいぶ生きていてメモとか実装が残っていることは良いなと思いました。

やったことを忘れてしまうことも多いし現場が変わっても見れるところにアウトプットしていきたいですね。

Discussion