コンテナでサーバーを動かして、それに対するリクエストをするE2Eテストを GitHub Actions 上で動かすことに苦労したので書く。
成果物repo: https://github.com/sadnessOjisan/e2e-gha
お題となるサーバー
コンテナに固めるから別に何言語でも良いので、まずはちょっとしたエコーサーバーを書いてみよう。
import Fastify from "fastify";
const fastify = Fastify({
logger: true,
});
fastify.get("/", async function handler(req, res) {
const q = req.query["q"];
res
.status(200)
.headers({
"content-type": "application/json",
})
.send({ q });
});
try {
await fastify.listen({ port: 3000, host: "0.0.0.0" });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
これはクエパラをそのまま返してくれるただのエコーサーバーだ。
❯ curl "http://localhost:3000?q=hogeeee"
{"q":"hogeeee"}
このエコーサーバーにテストを書く。
import { expect, test } from "vitest";
test("echo q=1", async () => {
const res = await fetch(`${process.env.ORIGIN}?q=1`);
const data = await res.json();
expect(data).toEqual({ q: "1" });
});
test("echo q=2", async () => {
const res = await fetch(`${process.env.ORIGIN}?q=2`);
const data = await res.json();
expect(data).toEqual({ q: "2" });
});
Origin 部分を環境変数にしているのは後々嬉しいからなので今は気にしないでほしい。このテストコードを走らせると、
# server を起動
yarn start
# test を起動
ORIGIN=http://0.0.0.0:3000 yarn test
その結果は
yarn run v1.22.19
$ yarn workspace:test test
$ yarn workspace test test
warning package.json: "test" is also the name of a node core module
$ vitest
DEV v0.34.3 /Users/ideyuta/Documents/projects/toybox/e2e-gha/packages/test
stdout | unknown test
http://0.0.0.0:3000
✓ integrate.test.ts (2)
✓ echo q=1
✓ echo q=2
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 04:42:53
Duration 204ms (transform 19ms, setup 0ms, collect 9ms, tests 26ms, environment 0ms, prepare 49ms)
PASS Waiting for file changes...
press h to show help, press q to quit
となる。ではこれを GitHub Actions 上で動かそう。
起動とテストを一つにまとめる
E2E なのでサーバーの起動とテストの実行の2つが必要だ。先は start と test でコマンドを二つに分けていたが、CI 上でのタスク実行においてサーバーを起動しぱなしにすると test コマンドを実行できないので、これができるように一つのコマンドにまとめてみる。
yarn start & ORIGIN=http://0.0.0.0:3000 yarn test
&
はバックグラウンドでプロセスを実行できる。
FYI: https://kazmax.zpp.jp/linux_beginner/process_background.html
ここでは yarn start
をバックグラウンドで動かし、それに対してフォアグラウンドで yarn test
を実行し、実際にサーバーにリクエストを送って E2E テストをしている。
これはそのままGitHub Actions 上でテストできる。
name: test
on: push
jobs:
test:
name: run test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: yarn install
- run: yarn start & ORIGIN=http://localhost:3000 yarn test
E2E テストはコンテナに対してすべきで、yarn start した結果のテストは成果物へのテストではない
さて、僕たちが成果物をコンテナデプロイしているのであれば先ほどのテストはよくないかもしれない。
テスト時は
name: test
on: push
jobs:
test:
name: run test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: yarn install
- run: yarn start & ORIGIN=http://localhost:3000 yarn test
で ubuntu 環境で動くサーバーにテストしているが、もしDockerfileが
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN yarn install
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder app/packages/server/src /app/src
COPY --from=builder app/node_modules/ /app/node_modules/
COPY --from=builder app/packages/server/package.json /app/package.json
EXPOSE 3000
CMD [ "src/index.js" ]
(初手 COPY . .
したり yarn focus してなかったり色々ダメダメだけど今は見逃して!)
のように distroless を使っている環境であれば、環境差異によるバグがあってそれにテストに気づけないということがあるかもしれない。私は E2E テストはコンテナに対してすべきだと思う。
コンテナを動かして、そこのE2Eテストする
というわけで例えば
version: "3"
services:
server:
build:
context: .
dockerfile: ./packages/server/Dockerfile
ports:
- 8080:3000
のような docker-compose.yaml を用意して、GitHub Actions 上で
name: test
on: push
jobs:
test:
name: run test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- run: yarn install # vitest 入れるために仕方なく
- name: Run Docker container
run: docker compose up -d server
- name: Run test
run: ORIGIN=http://localhost:3000 yarn test
としたくなるが、実はこれは動かない。
yarn test
の前にデーモンとして立てたサーバーに対して、同じワークフローからであっても前のstepには localhost でアクセスすることができない。
そういうことをするには GitHub Actions では Service Container という機能で実現できる。
FYI: https://docs.github.com/en/actions/using-containerized-services/about-service-containers
しかしこれをするには Docker Hub やら GitHub Registry に登録する必要がある。これはテストをまわしたい度にそこに登録するのはめんどくさかったり、所属企業によっては使えなかったりするかもしれない。なので Docker Network を使った解決法を考える。
docker-compose と docker network でテストする
まず先の方法を見て、Docker もバックグラウンド実行すればいいのではと思うかもしれない。ローカルで試してみると、
❯ docker compose up server & ORIGIN=http://localhost:8080 yarn test
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 11:59:25
Duration 256ms (transform 18ms, setup 0ms, collect 11ms, tests 29ms, environment 0ms, prepare 52ms)
✨ Done in 1.01s.
といった感じだ。うまくいきそう。
が GHA 上で試すと、
- name: Run Docker container
run: docker compose up server & ORIGIN=http://localhost:8080 yarn test
で
Serialized Error: { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '::1', port: 8080 }
と出力され、動かない。どうしてかはよく分かっていない。同じステップだから同じプロセスで繋がりそうなのにね。有識者に聞いたら「DockerはリモートホストとかもあるからGHAの実装次第では localhost で繋がらなくても不思議ではない」「Self Host の GHA 読んでみたら分かるかも」とのことだった。自分はわからなかったのでこの疑問には目を瞑って先に答えを書くと、こうなる。
docker-compose.yaml
version: "3"
services:
server:
build:
context: .
dockerfile: ./packages/server/Dockerfile
networks:
- networkForTest
ports:
- 8080:3000
test:
build:
context: .
dockerfile: ./packages/test/Dockerfile
depends_on:
- server
networks:
- networkForTest
environment:
- ORIGIN=http://server:3000
networks:
networkForTest:
driver: bridge
packages/test/Dockerfile
FROM node:18 AS builder
ENV ORIGIN=http://localhost:8080
WORKDIR /app
COPY . .
RUN yarn install
CMD [ "yarn", "test" ]
つまりテストの実行を別コンテナに押し込むのである。そうすると Docker Network を経由して、アプリケーションサーバーに対して http://${container name}:${container port}
でアクセスできるようになる。Docker Network のおかげで、アプリケーションサーバーのホストが発見可能になるのである。
まとめ
- プロセス2つ使って、サーバープロセスをバックプロセスで、テストをフォアグラウンドで実施する。これはGitHub Actions上でも動く。
- テスト対象のサーバープロセスをデプロイするコンテナ環境そのもので使いたいなら、docker-compose の network 機能を使うと良い。