はじめに
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する
- Step2. nodeのテストを実行できるようにpackage.jsonを設定する
- Step3. nodeのコード(Express)をテスト可能なようにリファクタリングする
- Step.4 実際にテストを書く
Step1. jest, supertestをinstallする
npm install --save-dev jest supertest
Step2. nodeのテストを実行できるように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
で動くか?検証するとよい。
describe('Sample Test', () => {
it('should test that true === true', () => {
expect(true).toBe(true)
})
})
Step3. nodeのコード(Express)をテスト可能なようにリファクタリングする
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 |
のように変わるだけ。
const express = require('express')
const app = express()
// Middlewares...
// Routes...
module.exports = app;
const app = require('./app')
app.listen(8081, () => {
console.log('listening on port 8081!')
});
Step.4 実際にテストを書く
ここまで出来たら実際にテストコードを書いてAPIテストを実行してみる。
テスト対象のコードは以下。
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;
テストコードは以下。
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のテスト実行結果は以下のようになり、ちゃんとテストできている事が分かる。
ただこのままではテストしてよくない・・・
上記のように実際にWeb APIをCallしてのテストは安定せず非推奨。
そこでmockの機能を使って通信をmock化する事でより実践的な(CI上で継続的にテストする)テストを書いてみる。
続きはJest・Supertestを使用したnode.js(Express)のAPIテスト 実践編を参照。
実際にmock化せず何回かテストをすると、成功する時もあれば失敗する時もあり安定しない。
また、今回はapp.js
にEnd Pointを記載していたが、routes
・conrollers
という構成がきれいで一般的なのでそちらのコードにリファクタリングしたものもJest・Supertestを使用したnode.js(Express)のAPIテスト 実践編(※現在執筆中なのでリンクは飛びません)で扱う。
トラブった事
ⅰ. ReferenceError: regeneratorRuntime is not defined
{
"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)を・・・で少し触れたが、以下のソースコードはエラーになる。
const express = require('express')
const app = express()
// Middlewares...
// Routes...
app.listen(8081)
ひとまずどうなるのか?を詳しく見ていくためにわざとこのままjestのテストを実行してみると、
のようにエラーになる。
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
を今度は実行してみる。すると、
のように、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.js
とserver.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)
})
})
実際にテストを書いたプロジェクト
参考文献
- Jest公式
- asyncを使用したコードをjestでテストすると「regeneratorRuntime is not defined」エラーが発生
- jest で Node.js の テストするなら testEnvironment: "node" を使う
- Testing NodeJs/Express API with Jest and Supertest
- Testing NodeJs/Express API with Jest and Super test
- Endpoint testing with Jest and Supertest
-
https://jestjs.io/ja/docs/configuration#testenvironment-string
https://qiita.com/yosuke_furukawa/items/8c14b7b4461cc109bc54
ただ、const cors = require('cors'); app.use(cors());
を使っていればCORS
の問題は発生しない。 ↩ -
https://qiita.com/quzq/items/1662bff594f05d52f206
webpackでbabelを使用しているプロジェクトだったが、asyncを使用したコードをjestでテストをした時に、ReferenceError: regeneratorRuntime is not defined
というエラーが発生した。
これは.babelrcにて、babelのターゲットを指定することで解決できる3。 ↩