Jest・Supertestを使用したnode.js(Express)のAPIテスト　app.listen()はエラーになる

## はじめに
Jest・SupertestでAPIテストを作成した際に躓いたので、その実装の備忘録を残しておく。
この記事ではnode.js(Express)のAPIテストを実行できるようになる部分までを記載した。より実践的にはAPIテストのエンドポイントのメソッドでWeb APIをcallしているような場合は、それをmock化してWeb APIがcallされないようにすべきであるが、それについては[mockを使ったNode.js(Express)のAPIテスト](https://qiita.com/yuta-katayama-23/items/2ee51d41153359e56335)を参照。
（実際にテストを作る中でトラブって困った事も[トラブった事](#トラブった事)として残した。）

## どうやってExpressのAPI(End Point)テストをjestでやるのか？
- [Step1. jest, supertestをinstallする](#step1-jest-supertestをinstallする)
- [Step2. nodeのテストを実行できるようにpackage.jsonを設定する](#step2-nodeのテストを実行できるようにpackagejsonを設定する)
- [Step3. nodeのコード(Express)をテスト可能なようにリファクタリングする](#step3-nodeのコードexpressをテスト可能なようにリファクタリングする)
- [Step.4 実際にテストを書く](#step4-実際にテストを書く)

### Step1. jest, supertestをinstallする
```console:npm
npm install --save-dev jest supertest
```

### Step2. nodeのテストを実行できるようにpackage.jsonを設定する
```json:package.json
{
  ・・・
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ]
  },
  "scripts": {
    "test": "jest",
  },
  ・・・
}
```
上記のjest configurationの意味について少し解説すると、

- **testEnvironment**<br>jestのテスト実行環境を指定するkey<br>デフォルトではブラウザで動くjavascriptをテストする事が多いので、ブラウザのような環境として`jsdom`が指定される<br>ただ、node.jsの機能をテストする場合、`node`を指定してnodeのような環境でテスト実行するように指定する。[^1]<br>これはnodeでHTTP通信をしておいてAPIテストを行う場合、`jsdom`のままだとhttp requestがXHRになってしまい、CORSの制約などの細々した制約を受けてしまうので、それを避けるため。
- **coveragePathIgnorePatterns**<br>テストのカバレッジ計測対象外にするディレクトリを指定できるkey

ここまでできたらひとまずよくあるjestのテストの初めの一歩で出てくるテストを実際に書いてみて、`npm run test`で動くか？検証するとよい。

```js:sample.test.js
describe('Sample Test', () => {
    it('should test that true === true', () => {
        expect(true).toBe(true)
    })
})
```
こんな感じに`PASS`になればjestの設定はOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/90c3348c-a5e4-fcfd-23de-364bc8450f2a.png)

### Step3. nodeのコード(Express)をテスト可能なようにリファクタリングする
[^エラー]: [トラブった所②](#ⅱ-applistenの部分でエラー)
まず、以下のような`node`のソースコードはエラー[^エラー]が発生してjestでテストできないので注意。

```js:app.js
const express = require('express')
const app = express()

// Middlewares...
// Routes...

app.listen(8081)
```

詳細は以下の[トラブった所の「ⅱ-applistenの部分でエラー」](#ⅱ-applistenの部分でエラー)を参照してほしいが、
簡単に言うと`listen`したportが開きっぱなしになりそれを閉じてやることができないのでずっとserverが起動した状態になりjestのテストが終了せず、以下のようなエラーが発生しテストが失敗する。

```
Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TCPSERVERWRAP

      25 |
      26 | // Setup Server
    > 27 | app.listen(8081, function () {
         |     ^
      28 |     console.log('listening on port 8081!')
      29 | })
      30 |

      at Function.listen (node_modules/express/lib/application.js:618:24)
      at Object.<anonymous> (src/server/server.js:27:5)
```

ではどうすればいいかだが、以下のように単純にファイルを分割してあげればいい。
Express serverの起動方法は、

| before | after |
|:-:|:-:|
| node app.js | node server.js |

のように変わるだけ。

```js:app.js
const express = require('express')
const app = express()

// Middlewares...
// Routes...

module.exports = app;
```
```js:server.js
const app = require('./app')

app.listen(8081, () => {
  console.log('listening on port 8081!')
});
```

### Step.4 実際にテストを書く
ここまで出来たら実際にテストコードを書いてAPIテストを実行してみる。
テスト対象のコードは以下。

```js:app.js
const express = require('express');
const app = express();

/* Middleware */
app.use(express.urlencoded({
    extended: false
}));
app.use(express.json());

// Cors for cross origin allowance
const cors = require('cors');
app.use(cors());

// Initialize the main project folder
app.use(express.static('dist'))

// dotenv
const dotenv = require('dotenv')
dotenv.config();

const axios = require('axios').default;

// instance for get countries and cities
const instance = axios.create({
    baseURL: 'https://api.countrystatecity.in/v1/',
    timeout: 2000,
    headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` }
})

app.get('/allCountries', async (req, res) => {
    try {
        const countries = await instance.get('countries')
        res.send({ countries: countries.data })
    } catch (error) {
        errorHandler(res, error)
    }
})

app.get('/allCitiesByCountry', async (req, res) => {
    try {
        const cities = await instance.get(`countries/${req.query.ciso}/cities`)
        res.send({ cities: cities.data })
    } catch (error) {
        errorHandler(res, error)
    }
})

const errorHandler = (res, error) => {
    if (error.response) {
        res.status(500).send(error.response.data)
    } else {
        res.status(500).send({ error: error.message })
    }
}

module.exports = app;
```

テストコードは以下。

```js:routes.test.js
const request = require('supertest')
const app = require('../../src/server/app')

describe('Get Endpoints (not mocking)', () => {
    it('/allCountries', async () => {
        const res = await request(app).get('/allCountries')
        expect(res.status).toEqual(200)
        expect(res.body.countries[0].name).toEqual('Afghanistan')
    })

    it('/allCitiesByCountry', async () => {
        const res = await request(app).get('/allCitiesByCountry?ciso=JP')
        expect(res.status).toEqual(200)
        expect(res.body.cities[0].name).toEqual('Abashiri')
    })
})
```

jestのテスト実行結果は以下のようになり、ちゃんとテストできている事が分かる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/d307133e-183b-d869-d7a1-3e5a656c5edc.png)

### ただこのままではテストしてよくない・・・
上記のように**実際にWeb APIをCallしてのテストは安定せず非推奨**。
そこでmockの機能を使って通信をmock化する事でより実践的な（CI上で継続的にテストする）テストを書いてみる。
続きは[Jest・Supertestを使用したnode.js(Express)のAPIテスト　実践編]()を参照。

実際にmock化せず何回かテストをすると、成功する時もあれば失敗する時もあり安定しない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0aa51cca-e5da-bfb1-8bb5-e5f66bacc42c.png)

また、今回は`app.js`にEnd Pointを記載していたが、`routes`・`conrollers`という構成がきれいで一般的なのでそちらのコードにリファクタリングしたものも[Jest・Supertestを使用したnode.js(Express)のAPIテスト　実践編（※現在執筆中なのでリンクは飛びません）](#)で扱う。

## トラブった事
### ⅰ. ReferenceError: regeneratorRuntime is not defined
[^babel]: https://qiita.com/quzq/items/1662bff594f05d52f206
webpackでbabelを使用しているプロジェクトだったが、asyncを使用したコードをjestでテストをした時に、`ReferenceError: regeneratorRuntime is not defined`というエラーが発生した。
これは.babelrcにて、babelのターゲットを指定することで解決できる[^babel]。

```json:.babelrc
{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}
```

※これは単純に`babel`を使っている時にasync/awaitを書くときにはそれを解釈する(トランスパイルする)ための指示みたいなものが必要だからで以下をapp.jsに追記するでも対応可能。

```
// for using async/await in babel
require('core-js/stable')
require('regenerator-runtime/runtime')
```

### ⅱ. app.listen('・・・')の部分でエラー
上記の[Step3. nodeのコード(Express)を・・・](#step3-nodeのコードexpressをテスト可能なようにリファクタリングする)で少し触れたが、以下のソースコードはエラーになる。

```js:app.js
const express = require('express')
const app = express()

// Middlewares...
// Routes...

app.listen(8081)
```

ひとまずどうなるのか？を詳しく見ていくためにわざとこのままjestのテストを実行してみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/61175e5b-10c0-2a20-3c46-8b368cbf221d.png)
のようにエラーになる。
logに出ている内容としては色々あるが、

- A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks.<br>ワーカープロセスが正常に終了できず、強制的に終了しました。 これは不適切な分解によるテストのリークが原因である可能性があります。 --detectOpenHandlesを指定して実行し、リークを見つけてください。

と言っているので、`jest --detectOpenHandles`を今度は実行してみる。すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/1ba5c29d-0c03-4ce2-43ee-fd464993b289.png)
のように、jestのテストが終了せずずっと実行されたままの状態になってしまう。

- Jest has detected the following 1 open handle potentially keeping Jest from exiting:<br>Jestは、次の1つの開いたハンドルを検出したため、Jestが終了しない可能性があります。

と言っているようにjestのプロセスが終了しないためエラーになっており、これは`app.listen(8081)`でserverを起動してしまいそれがずっと生きている事が原因。なので[Step3. nodeのコード(Express)を・・・](#step3-nodeのコードexpressをテスト可能なようにリファクタリングする)で書いたように`app.js`と`server.js`とに分割する必要がある。

※ここで実行していたテストコードは以下。

```js:routes.test.js
const request = require('supertest')
const app = require('../../src/server/app')

describe('Get Endpoints', () => {
    it('/allCountries', async () => {
        const res = await request(app).get('/allCountries')
        expect(res.status).toEqual(200)
    })
})
```

## 実際にテストを書いたプロジェクト
https://github.com/yuta-katayama-23/travel-app/tree/2d9fc86d1d9c477eef9cd7adaecc411ec3e6cbc6

## 参考文献
- [Jest公式](https://jestjs.io/ja/docs/configuration#testenvironment-string)
- [asyncを使用したコードをjestでテストすると「regeneratorRuntime is not defined」エラーが発生](https://qiita.com/quzq/items/1662bff594f05d52f206)
- [jest で Node.js の テストするなら testEnvironment: "node" を使う](https://qiita.com/yosuke_furukawa/items/8c14b7b4461cc109bc54)
- [Testing NodeJs/Express API with Jest and Supertest](https://dev.to/nedsoft/testing-nodejs-express-api-with-jest-and-supertest-1km6)
- [Testing NodeJs/Express API with Jest and Super test](https://dev.to/mhmdlotfy96/testing-nodejs-express-api-with-jest-and-supertest-1bk0)
- [Endpoint testing with Jest and Supertest](https://zellwk.com/blog/endpoint-testing/)

[^1]: https://jestjs.io/ja/docs/configuration#testenvironment-string<br>https://qiita.com/yosuke_furukawa/items/8c14b7b4461cc109bc54<br>ただ、`const cors = require('cors');　app.use(cors());`を使っていれば`CORS`の問題は発生しない。