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

# 本記事の目的
前回に引き続き、docker-composeでのインテグレーションテストに用いるためのアプリを作成します。今回で実装は完成し、次回からはインテグレーションテストの作成を行います。

[前回の記事](https://qiita.com/youk-h/items/23a724d226e3747b1e41)では、REST APIを受け付けるためのサーバーを作成しました。今回の記事では、Postgresコンテナのセットアップを行います。そのあとで、このpostgresコンテナとやり取りするREST APIサーバーの中身をNode.jsで作成します。

[次回の記事](https://qiita.com/youk-h/items/928694f4a07c96ec9a3c)では、docker-composeを用いてアプリを一コマンドで立ち上げる内容を見ていきます。

ソースコードは以下に置いています
https://github.com/youk-h/Integration-test
アプリはrest-api-serverディレクトリに入れています。

# 対象読者
・Node.jsを用いた実装に興味がある方
・dockerの利用例の一例を知りたい方

# アプリの動作
(例)
POST /cars  で　bodyにJSON形式で{"name": "demio","maker": "mazda" }を詰めるとデータベースに登録されます。
GET  /cars/demio では 上で登録した内容が取得できます。
put  /cars/demio でbodyに更新データを詰めるとデータのアップデートがされます
delete /cars/demio を送ると、データベースから指定したデータを削除します。

# Postgresコンテナを立ち上げる
テーブル作成のDDLとコンテナBuild用のDockerfileを準備します。

## テーブル作成用のDDLを書く
データベースにテーブルを作るための DDL を create-car-table.sql という名前で作ります。

今回は CAR というテーブルを作ります。
カラムには
name(車の名前)
maker(メーカー)
を入れています。

```sql
CREATE TABLE CAR(
  name TEXT NOT NULL,
  maker TEXT NOT NULL
);
```

## Postgresコンテナを立ち上げる
Postgresのコンテナ起動には、Docker Hubで公式イメージが公開されているのでそれを使います。
Dockerfileは以下のように書きます。
以下の設定では、
デフォルトデータベース: postgres
スーパーユーザー: postgres
ログインパスワード: postgres
で起動されます。

```yml
# Dockerfile-postgres
# postgresの公式イメージ v10.6 を用います
FROM postgres:10.6

# /docker-entrypoint-initdb.d/にコピーしたsqlはコンテナ起動時に実行されます。
COPY ./ddl /docker-entrypoint-initdb.d/
```

セットアップが完了したコンテナ起動の実行手順
1. Dockerfileと同じ階層に、ddlディレクトリを作り、その中に上で作ったcreate-car-table.sqlを入れます。
2. 以下のコマンドを実行して、postgresイメージを作成する(-t はイメージの名前を決めるオプション)
`docker build -t postgres:e2e -f Dockerfile-postgres .`
3. 以下のコマンドで作成したイメージを使って、コンテナを立ち上げます。
（ポートは5432でホストからフォワーディングしています）
`docker run -it -d --rm --name postgres-e2e -p 5432:5432 postgres:e2e` 

# REST API サーバ作成
サーバーは、リクエストを受け取ってデータベースにクエリを投げるまでに以下の4層の階層構造で作成します。
REST API インターフェース　-> REST API処理用のハンドラー　->　SQL準備レイヤー　->　SQL実行レイヤー

## （1階層目）REST API インターフェース
1階層目ではREST API リクエストを受け付けるためのをインターフェースを作成します。
リクエストボディはapplication/jsonで受け付けます。

今回は例として、車リソースの操作を行います。

```typescript
// router.ts
// コメントのないところは、前記事を参照してください
import * as express from "express";
import * as restApi from "./handler/rest-api";

const app = express();
const port = 3000;

const restApiRouter = express.Router();

// リクエストのボディをapplication/jsonで受け取るために書いています。
app.use(express.json());

app.all("*", restApiRouter);

// HTTPメソッドとリソース毎に、対応するハンドラーにルーティングしています
restApiRouter.get("/cars/:name", restApi.getHandler);
restApiRouter.post("/cars", restApi.postHandler);
restApiRouter.put("/cars/:name", restApi.putHandler);
restApiRouter.delete("/cars/:name", restApi.deleteHandler);

app.listen(port, () => console.log(`listen to ${port}`));
```

## （2階層目）REST API処理用のハンドラー
2階層目では、1階層目からルーティングさらた REST API の処理を行うためのハンドラーを作成します。
長くなるので、HTTP GET /cars/:nameに対応するハンドラーgetHandlerを載せています。POST、PUT、DELETEのハンドラーも作成しています。

2階層目のこのハンドラーでは、3階層目のサービスが処理を行うためのパラメータを準備して渡し、その後サービスから返ってきたレスポンスをリクエストしてきたユーザーに返しています。この2階層目までが、ユーザーを意識した処理を書く階層になります。3階層以降ではデータベースを意識した処理を行っていきます。

ここでは、非同期処理のために、RxJSを用いています。
コード内ではgetCar$().subscribeの部分がそうです。（RxJSについては4階層目で見ていきます）
この書き方では、getCar$というサービスに任せた処理が完了したタイミングでsubscribe内の処理が実行されます。
RxJSを用いることで、このハンドラーでは非同期処理をほかのモジュールに任せて自分では行わず、終わったときに結果だけもらうというシンプルなつくりを実現できます。

```typescript
// rest-api.ts
import { Request, Response } from "express";

// ハンドラーのインターフェースを定義している（コードは下にあります。）
import * as models from "./rest-api.i";

// 3階層目のサービスをimportしている
import { CarManager } from "../service/car-crud";

export function getHandler(req: Request, res: Response) {
  // 3階層目に渡すパラメータの準備
  const params: models.GetCarParams = {
    name: req.params.name
  };

　// 3階層目のサービスに処理を任せている
  new CarManager().getCar$(params).subscribe(
    // サービスから返ってきた結果をレスポンスしている
    (result) => res.json(result).end(),
    (error) => res.status(error.code).send(error.message).end(),
  );
};
```

```typescript
// rest-apu.i.ts
// 上のコードのサービスに渡すインターフェースの定義
export interface GetCarParams {
  name: string;
}
```

## （3階層目）SQLの準部レイヤー
長くなるので、この階層もHTTP GET に関する処理だけを書いています。

この階層からデータベースを意識した処理を書いていきます。
まず、このgetCar$が何を受け取り何を返すかを表すインターフェースを定義しています。（関数の引数がGetCarParamsで戻り値がObservableのGetCarResult）
ここでは、SQLとその中で使うプレースホルダを作成してデータベースがトランザクションを行うための準備をし、4階層目にデータベースへのクエリを任せています。そして、クエリが終わった後の結果を4階層目から受け取って、それをフォーマットして2階層目に返しています。ここでも、RxJSを用いて3階層目はクエリが終わるのを待つだけです。また、4階層目のデータベースを抽象化しており、Postgresに限定しないように書いています。こうすることで、データベースをmysqlなどに変えたときにも、この3階層目の処理は書き換える必要がなく、変更箇所の局所化が行えます。変える必要があるのはDatabaseクラスの中の処理だけです。

```typescript
// car-crud.ts 
export class CarManager {
  public getCar$(params: models.GetCarParams): Observable<models.GetCarResult> {
    // SQLを保存したファイルまでのパスを書いています。
    const filePath = path.join(__dirname, "../../sql/select-car.sql");
    // SQLに渡すプレースホルダを作っています。
    const placeHolder = [
      params.name,
    ];

    return Database.transaction$(filePath, placeHolder).pipe(
      // クエリが終わった後の結果を受け取り、フォーマットして返している
      map((result: QueryResult) => ({
        name: result.rows[0].name,
        maker: result.rows[0].maker,
      })),
      catchError(() => {
        const err = {
          code: 404,
          message: "Not Found",
        };
        return throwError(err);
      }),
    );
  }
}
```

``` typescript
// car-crud.i.ts
// 3階層目のサービスのインターフェースを定義している
export interface GetCarParams {
  name: string;
}

export interface GetCarResult {
  name: string;
  maker: string;
}
```

## （4階層目）SQL実行レイヤー
ここでは、RxJSについて少しみていきます。RxJSで最も大事なのはオブザーバブルというデータの流れを準備する事です。このデータの流れを準備することで、非同期処理で流れてきたデータを扱うことができます。このオブザーバブルは、指定した非同期処理の結果を受けるための場所のイメージです。今の場合は、Postgresのクエリという非同期処理の結果を受け取るためにオブザーバブルを作っています。オブザーバブルを作るために,ofとfromをが用意されています。ofはオブザーバブルでないものをオブザーバブルに変える関数です。fromは配列やPromiseをオブザーバブルに変える関数です。
また、RxJSはデータを受け取るオブザーバブルだけでなく、そのデータを受け取った後に行う処理を行えるオペレータを提供しています。今回は、mergeMap, catchError, finalizeというものを用いています。非同期処理を決まった順番で行いたい場合はmergeMapを連ねていけば行えます。また、この一連の処理の中のどこかでエラーが生じた場合にはcatchErrorというオペレータでキャッチします。そして、正常、エラー問わず処理が完了したときにfinalize内の処理が走ります。

上記の事を用いて、Postgresへのクエリの非同期処理を行いました。

Postgresへの接続とクエリを行うために、pgというモジュールを以下のコマンドでインストールしています。
`npm install -s pg`
また、RxJSを用いるために、rxjsを以下のコマンドでインストールしています。
`npm install -s rxjs`

``` typescript
// SQLをファイルから読み込むためにimportしています
import * as fs from "fs";

// rxjs を使うためにimportしています
import { of, throwError, from, Observable } from "rxjs";
import { mergeMap, catchError, finalize } from "rxjs/operators";

// postgresをnode.jsから操作するためにimportしています
import { Pool, QueryResult, PoolClient } from "pg";

export class Database {
  // データベースへの接続を行う処理です。
  public static connect$(): Observable<PoolClient> {
    // ここでは、接続先のプールを準備しています。
    //上で見たDockerfileで立ち上げたPostgresコンテナならこの設定で接続できます。
    const pool = new Pool({
      user: "postgres",
      password: "postgres",
      database: "postgres",
      host: "localhost",
      port: 5432,
    });

    // ここで、プールに接続しています。
    return from(pool.connect());
  }

  public static transaction$(filePath: string, placeHolder: any[] = []): Observable<QueryResult> {
    // sqlをファイルを同期的に読み取って、それを文字列に変形しています
　　const sql = fs.readFileSync(filePath).toString();

　　 // 以下では、トランザクションを確立してクエリを投げています
    return Database.connect$().pipe(
      mergeMap((client) => of(client).pipe(
        // トランザクションを開始しています
        mergeMap(() => client.query("BEGIN")),
        // ここで、SQLを実行してデータベースの操作を行っています
        mergeMap(() => from(client.query(sql, placeHolder)).pipe(
          mergeMap((response: QueryResult) => {
　　　　　　 // クエリが成功したらコミットしています
            client.query("COMMIT");
            return of(response);
          }),
        )),
　　　　// クエリがエラーになったときに、ここでcatchされます
        catchError((error) => {
          client.query("ROLLBACK");
          return throwError(error);
        }),
　　　　 // ここが最終処理で接続をリリースしています
        finalize(() => client.release()),
      )),
    );
  }
}
```

# まとめ
今回で、REST APIリクエストを受け取って、データベースとデータをやり取りする処理までを実装できました。いよいよ、docker-composeを用いたインテグレーションテスト作成の準備ができました。次回からは、インテグレーションテストを作成していきます。