始めに
apiのnodeのバージョンを8から14にしました(まだリリースはしてない)!!
14への移行自体は簡単だったもののテストの移行でかなり苦戦しました。。。
テストは古のライブラリmocha-coで書かれており、仕方なくmocha-coのシンタックスをjestに置き換える作業を開始しました。これが予想以上にめんどくさくその時のHow toを解決する方法がこの記事になります。
以下の問題にお悩みの方はこちらを参考していただけるかと思います。またいい方法をご存知の方はお教えください!!
- TooManyConnectionとなってしまう
- 毎回のテストでサーバー立ち上げとクローズをするのめんどくさい
- --ranInBandつけたけどなんか挙動がよくわからない・・・。
- mochaからのjestに移行したい
正確にjestのランタイムの仕様が分かっているわけではなく、調べたり試したりした結果なので間違いやもっと詳細が分かる方がいらっしゃいましたらぜひお教えください!!
また正確なコードを書くと記述量が多いので雰囲気で書いているところもあるのでご了承ください。
前提条件
- テスト用のDBを立ててデータを流しこみながら行っている
- モジュール単位のテストではなくApiに対してリクエストを投げてそのレスポンスとDBのデータを確認してテストを行う
といったことを前提とします。データベースはインメモリではなくデータベースサーバーを立てる前提です。ですのでテストは順次実行をしていく必要があります。(もともとそうなっていたのが理由としては大きいですが)実際の環境に近い環境を立てることができることがこの作りのメリットだったりします。逆にインメモリにすると並列でテストを実行することができるので、実行速度をあげることができます。
では、データベースサーバーを立てるテストの場合、順次実行をしていかないといけない理由としては、
こんな感じに一斉に更新がかかると他のテストに影響が出ることがあります。
たとえば、テスト前にテーブルを消したり、データの更新をしたりとか・・・。すると想定されていた状態にないデータが生まれテストが落ちたします。しかも恐ろしいことにそれが処理時間によって変わるのでテストガチャが生まれます。
そのためこの場合は、順次実行をしていくのがよいです。
こうすればテストファイルが一斉に更新されることによっておこるデータベースの不整合を防ぐことができます。
そのためのオプションとして、jestでは--runInBand
オプションを利用します。
jest --runInBand
ただし、これだけでは問題が発生します。
テスト用のAPIサーバーが立ち上がらない or 立てたり切ったりし続けると遅い問題
具体的にどういう問題かについての前にAPIテストで行うことを書きます。
APIテスト
APIのテストでは
- モジュール単位でテストする方法
- APIサーバーを立ててリクエストを送ることによって
があります。後者の方法で有名なライブラリとしてはsupertest
が挙げられます。
例えばexpress
とsupertest
の両方を使ってサーバーを立てるコードは下記のように書けます。
// app.jsconstexpress=require('express');constapp=express();app.get('/',function(req,res){res.send('Hello World');});module.exports=app;
// test-request.jsconstsupertest=require('supertest');constapp=require('./app');module.exports=supertest.agent(app.listen(3000));
// index.test.jsconsttestRequest=('./test-request');describe('test',()=>{it('200?',()=>{testRequest.get('/').expect(200);});});
またテスト用のデータを流し込むためのモジュールも下記のように定義します。
// test-model.jsconstSequelize=require('sequelize');constsequelize=newSequelize('test','test','test',{host:'127.0.0.1',dialect:'mysql',});constmodels={...};module.exports={sequelize:sequelize,models:models};
mochaの場合はtest-requestやtest-modelを読み込んでテストをすればそれでよく、問題は発生しません。
しかし、jestの場合はそうはいきません。
mochaにおける挙動
といった形になります。そのため先ほどのコードで一度しかコネクションはできないですし、一度しかサーバーは立ち上がりません。
jestにおける挙動と問題
jestにおいてはどのようになるのかというと、
といった形になります。そのためmochaのときのような書き方はやめてテストファイルごとに『立てて、切って』とする必要があります(書き換えの際はめっちゃ大変です)。
とはいえ、毎回そのようにするのは結構大変ですし、毎回『起動、コネクト、停止、切断』と実行していくとテスト完了にかかる時間が増えてしまいます。
- 毎回のテストでサーバーの起動・停止、dbへのコネクション・停止をする必要がある
- 記述量が増え、closeを忘れるとテストが落ちる
- 処理が増えるため必然的に実行時間が長くなる
- エラーハンドリングをしてcloseしなくても動くようにする
- 大量のコネクションが貼られることになりtoo many connectionとなってしまう
といった問題が発生します。
そのためには、globalSetupとtestEnvironmentを使います。
globalSetupを使い一度のみ起動する
globalSetupを使います。
jestではこういった形で各ファイルが実行されるため、一度しか実行されないglobalSetupでサーバーの起動をなどを行うのが良いかと思われます。
// globalSetupconstsupertest=require('supertest');constapp=require('./app');global.__request=supertest.agent(app.listen(3000));
// test-utils/request.jsmodule.exports=global.__request;
// test.tsimportrequestfrom'test-utils/request';describe('test',()=>{it('200',()=>{request.get('/').expect(200);});});
気持ち的にこんな感じにglobalにロードしたモジュールを保存させてテストで利用できるようにしたいんですが、ここにはここで落とし穴があります。その落とし穴はsetUpFilesにあります。
setUpFilesでは何を行っているのかというか『あたらしい環境のセットアップです』具体的にはglobalをはじめとしたテスト実行用の環境を生成するタイミングになり、新しくglobalを作っているため、下記のようなコードの場合、
// globalSetUpglobal.__global='I am from global setup';
// setUpFilesconsole.log(global.__global);// <- 新しいコンフィグになるためundefinedglobal.__setUp='I am from setup files';// <- ただしここで追加したglobalメンバーにはテストファイルがアクセス可能
となります。そのため、実際のテストファイルでも
// test.jsdescribe('test',()=>{console.log(global.__global);// undefinedconsole.log(global.__setUp);// I am from setup files;
となります。
親玉のランナーの実行環境とは別の環境が作られるというところがポイントになります。
setUpAfterEnvもタイミングは異なれど同じ挙動をします。
そこでどうするのかというとtestEnvironmentを利用します。
testEnvironment
testEnvironmentを使うことで柔軟にテスト環境を構築することができます。
// package.json"testEnvironment":"./my-custom-environment.js",
// my-custom-environmentconstNodeEnvironment=require('jest-environment-node');classCustomEnvironmentextendsNodeEnvironment{constructor(config,context){super(config,context);}asyncsetup(){this.global.__request=global.__request;awaitsuper.setup();}}module.exports=CustomEnvironment;
このようにsetupメソッドでglobal.__request
を this.global__request
に設定することによってテストファイルの方で global.__request
にアクセスすることができます。
まとめ
最終的にはこのようになります。
アプリケーションコード
// app.jsconstexpress=require('express');constapp=express();app.get('/',function(req,res){res.send('Hello World');});module.exports=app;
// model.jsconstSequelize=require('sequelize');constsequelize=newSequelize('test','test','test',{host:'127.0.0.1',dialect:'mysql',});constmodels={...};module.exports={sequelize:sequelize,models:models};
テストユーティリティコード
// test-request.jsmodule.exports=global.__request;
// test-modelmodule.exports=global.__db;
// global-setup.jsconstsupertest=require('supertest');constapp=require('./app');constdb=require('model');global.__request=supertest.agent(app.listen(3000));global.__db=db;
// my-custom-environmentconstNodeEnvironment=require('jest-environment-node');classCustomEnvironmentextendsNodeEnvironment{constructor(config,context){super(config,context);}asyncsetup(){this.global.__request=global.__request;this.global.__db=global.__db;awaitsuper.setup();}}module.exports=CustomEnvironment;
// jest.config.json{"testEnvironment":"./global-setup.js","globalSetup":"./my-custom-environment.js"}
テストコード
// test.jsconsttestRequest=('./test-request');consttestModel=('./test-model');describe('test',()=>{beforeAll(async()=>{awaittestModel.reset();});it('200?',()=>{testRequest.get('/').expect(200);});});
全体図
問題点
ただし、jestのmockと相性が悪いという問題点があります・・・。
というのもglobal上にサーバーコードがロードされているため、それらを実行したい時は各ファイルごとに一回サーバーを落としてもう一度ロードする必要があります。
そのため、globalにこれらをやるための入り口を開けてあげる必要がありました。
ここら辺みなさんどうしてるんですかね?いろいろとご意見伺いたいのもあって書かせていただきました。