docker-composeを使ってNode.jsアプリのインテグレーションテストを行う #3

# 本記事の目的
[docker-composeを使ってNode.jsアプリのインテグレーションテストを行う #2](https://qiita.com/youk-h/items/3827694d084e20687f90)　ではテストに用いるREST APIサーバーアプリを作成しました。

今回はdocker-composeを用いて、Node.jsアプリとPostgreSQLをコンテナで立ち上げて通信を行う方法を紹介します。

[次回](https://qiita.com/youk-h/items/165d4aa4bc50dac6455d)は、この方法でデプロイしたアプリのインテグレーションテストを作成して動作確認を行っていきます。


今回の注目点
・TypescriptファイルをJavascriptファイルへトランスパイルする
・Node.jsアプリをDockerビルドするときにキャッシュを用いる
・`docker-compose up`　実行時に、毎回アプリのDockerfileをビルドする（変更後の最新のアプリのビルドのため）

ソースコードは[ここ](https://github.com/youk-h/Integration-test)に置いています。

# 重要なファイルの階層

インテグレーションテストのために用意するファイルはすべてe2eフォルダに入れます

```
root---- dist   ← アプリビルド後の実行ディレクトリ
     |
     | - e2e --- docker-compose.yml　
     |        |
     |        |- Dockerfile-postgres
     |
     | - rest-api-server　　← REST API サーバーアプリ
     |
     | - Dockerfile-app     ← アプリビルド用Dockerfile

```

# REST APIサーバのコンテナ作成
まず、Typescriptで作成したREST APIサーバアプリをJavascriptファイルにトランスパイルします。
その後、Dockerfileを書いてコンテナbuildを行います。

### REST APIサーバアプリのコンパイル
まず、コンテナ作成に先立って Typescript ファイルを Javascript ファイルへトランスパイルしておきます。
前回までは `ts-node` コマンドを用いて Typescript ファイルを直接実行していました。
しかし、この方法ではトランスパイルを行うため直接 Javascript ファイルを実行するよりも遅くなります。

コンテナは実環境での動作が求められるので、Javascript ファイルへトランスパイルしてすぐに実行できるようにしておきます。

以下のコマンドで、トランスパイルに必要な二つのモジュールをインストールします
`npm i -D tsc @types/node`

tsc　はトランスパイル用のコマンドです。
@types/nodeはNode.js組み込みモジュールの型定義モジュールです。
これを用いることで、import でのモジュールの依存関係をtscが解決できるようになります。

以上の準備の下、トランスパイルを以下のコマンドで行います。
`tsc ./[アプリの起動ファイル名].ts --outDir dist/`

[アプリの起動ファイル名]は、そのアプリの根幹のファイル名です（僕の場合はrouter.tsという名前です）
--outDir　オプションは、トランスパイル後のファイル群の置き場所です。

dockerコンテナビルドのために、npm script　に上記のトランスパイルコマンドを書いておきます。
npm scriptはpackage.json内のscriptsという部分に登録します。
package.jsonのscripts内に、build という名前で上記のコマンドを登録すると、上記のコマンドは、ターミナルで以下のコマンドを打つことで実行できます。
`npm run build`

dockerfileではこのbuild コマンドを使って、トランスパイルを行います。


## dockerfile作成
次に、このアプリのDockerfileを書きます。

```yml
# このコンテナのベースイメージです。今回はnodeのv10.16.0を使います。
FROM node:10.16.0

# このコンテナのユーザーを定義しています。
USER node

# 最初はnpm install を行うためのpackage.jsonだけをコンテナ内の/appにコピーします。
# こうすると、package.jsonに変更がなければ、npm installは次回からはキャッシュが使われます。
COPY --chown=node ./package.json /app/
# このコンテナの活動ディレクトリを指定しています。
WORKDIR /app

# package.jsonから依存関係を読み取って、アプリに必要なモジュールをインストールします
RUN npm install

# npm installが終わってからアプリのコードを/appにコピーしています。
COPY --chown=node . /app

# そして、前節でpackage.jsonに登録したbuildコマンドでトランスパイルを行っています
RUN npm run build

# build完了後に/dist内にアプリのjavascriptファイルがあるので、それをnodeコマンドで実行しています。
# javascriptファイルを、サーバーで起動するにはnodeコマンドを使います。
CMD ["node", "dist/[アプリの起動ファイル名].js"]
```

# コンテナ群を立ち上げる
e2eフォルダ内にdocker-compose.ymlファイルを作成し、以下のように書きます。

```yml
# docker-composeのバージョン
version: "3"
# 立ち上げるコンテナをservices以下に構成する
services:
# postgresコンテナを立ち上げる設定を書きます
# IPアドレスを意識しなくても、docker-composeで立ち上げた他のコンテナはここで指定した名前でアクセスできます
  postgres-e2e:
# e2eフォルダ内に置いたdockerfile-postgresをビルドするための命令です
    build:
      context: ./
      dockerfile: Dockerfile-postgres
# ここでの名前は、コンテナ自体の名前で、ユーザーがターミナルでこのコンテナを指定する名前はここで決めます
    container_name: postgres-e2e

# rest-api-serverを立ち上げます
  rest-api-server-e2e:
# build は一つ上の階層にあるDockerfile-appを用いて行う設定を書いています
    build:
      context: ../
      dockerfile: Dockerfile-app
    container_name: rest-api-server-e2e
# ここでは、ホストからコンテナへのフォワーディングポートを書いています。
# この設定では、localhost:3000でアクセスすると、このコンテナのポート3000にアクセスできます
    ports:
      - 3000:3000
```

上のymlファイルを以下のコマンドで実行するとデータベースとアプリが立ち上がります。
`docker-compose up -d --build`

up はymlファイルを読んでコンテナ群を立ち上げる命令です
-d はバックグラウンドで実行するためのオプションです
--build はyml ファイル内のbuildを毎回実行するためのオプションです

--build が無いと、ymlファイル内のbuildが古いイメージがあると実行されません。
それによって、アプリの内容を更新したのにbuildされずに古いアプリが使われる問題が生じます。
これを解決するために、--buildをつけています。

毎回buildされると効率が悪くなりますが、[上で書いたdockerfile](#dockerfile作成)では時間のかかるnpm installを前回のキャッシュが使われるように書いているので、時間のかかる処理はビルドの部分だけになります。
このように、Dockerfileを書く時にはキャッシュを使えるように工夫することでより効率的にコンテナ技術を使うことができます

# docker-composeを用いることによる実装の変更点
前回はホストマシンからDBコンテナにアクセスする際には、ホストとDBコンテナをポートフォワーディングで通信をしていました。
コードは以下のようになっていました。

``` typescript
  public static connect$(): Observable<PoolClient> {
    const pool = new Pool({
      user: "postgres",
      password: "postgres",
      database: "postgres",
      // DBにアクセスするために、localhostを指定していた
      host: "localhost",
      port: 5432,
    });
```

今回は、docker-composeを用いてコンテナ同士はyml ファイルで指定したサービス名で名前解決できるので以下のように書き換える必要があります。
このように、docker-composeを用いるとアクセス先の名前が変わります

``` typescript
  public static connect$(): Observable<PoolClient> {
    const pool = new Pool({
      user: "postgres",
      password: "postgres",
      database: "postgres",
      // yml ファイルでDBコンテナの名前をpostgres-e2eにした！！
      host: "postgres-e2e",
      port: 5432,
    });
```

# アプリの起動
今までは、dockerfile-postgresを実行してDBを立ち上げ、次にアプリをts-nodeを用いて実行していました。
でも今は、`docker-compose up -d --build`を実行するだけでアプリの起動が自動的に行われます。

以下のHTTPリクエストを実行するとDBにデータを挿入することができます。
POST localhost:3000/cars のbodyに{"name": "demio", "maker": "mazda"}
GET localhost:3000/cars/demio でデータの取得もできます

# まとめ
今回はdocker-composeを用いて必要なアプリ群を立ち上げました。
インテグレーションテストを行う際にもこの方法でアプリ群を立ち上げます。
次回はテストフレームワークJestを用いてテストを書き、インテグレーションテストを作成します。