20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-05-26

はじめに

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

どうやってExpressのAPI(End Point)テストをjestでやるのか?

Step1. jest, supertestをinstallする

npm
npm install --save-dev jest supertest

Step2. nodeのテストを実行できるようにpackage.jsonを設定する

package.json
{
  ・・・
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": [
      "/node_modules/"
    ]
  },
  "scripts": {
    "test": "jest",
  },
  ・・・
}

上記のjest configurationの意味について少し解説すると、

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

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

sample.test.js
describe('Sample Test', () => {
    it('should test that true === true', () => {
        expect(true).toBe(true)
    })
})

こんな感じにPASSになればjestの設定はOK。
image.png

Step3. nodeのコード(Express)をテスト可能なようにリファクタリングする

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

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

app.listen(8081)

詳細は以下のトラブった所の「ⅱ-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

のように変わるだけ。

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

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

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

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

Step.4 実際にテストを書く

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

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;

テストコードは以下。

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

ただこのままではテストしてよくない・・・

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

実際にmock化せず何回かテストをすると、成功する時もあれば失敗する時もあり安定しない。
image.png

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

トラブった事

ⅰ. ReferenceError: regeneratorRuntime is not defined

.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)を・・・で少し触れたが、以下のソースコードはエラーになる。

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

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

app.listen(8081)

ひとまずどうなるのか?を詳しく見ていくためにわざとこのままjestのテストを実行してみると、
image.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.
    ワーカープロセスが正常に終了できず、強制的に終了しました。 これは不適切な分解によるテストのリークが原因である可能性があります。 --detectOpenHandlesを指定して実行し、リークを見つけてください。

と言っているので、jest --detectOpenHandlesを今度は実行してみる。すると、
image.png
のように、jestのテストが終了せずずっと実行されたままの状態になってしまう。

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

と言っているようにjestのプロセスが終了しないためエラーになっており、これはapp.listen(8081)でserverを起動してしまいそれがずっと生きている事が原因。なのでStep3. nodeのコード(Express)を・・・で書いたようにapp.jsserver.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)
    })
})

実際にテストを書いたプロジェクト

参考文献

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

  2. トラブった所②
    まず、以下のようなnodeのソースコードはエラー2が発生してjestでテストできないので注意。

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

20
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?