expressとsequelizeを使ったWebアプリのテストをmochaで書いていろいろとハマったのでメモしておきます。
私が間違って認識してたり、もっといい方法があったりしたら、コメントで教えくれるとすごくうれしいですm(_ _)m
非同期処理は終了を待たずに終わる
sequelizeを使ったモデルの検索とか保存処理は全部非同期で処理されるので、何も考えずに書くと意図しない結果になります。
// 良くない例01
it('ユーザーを保存する', () => {
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
})
})
sequelizeのモデルが実行するcreate
とかfindAll
とかの関数は全部非同期に実行されて、Promiseを返すので、上の例ではUser.create
が呼ばれた瞬間もうテストが終了してしまいます。then
関数は「Promiseの実行が終わったときに呼ばれる関数を登録する」っていうだけなので、assert
の実行を待たずにテストが終了してしまいます。
非同期処理が絡むテストは最後にdoneを呼ぶ
it
が受け取る関数のひとつめにdone
を設定して、それをテストの最後に呼ぶように修正します。このようにすると、mochaはdone
が呼ばれるまでテストの終了を待ってくれるので、assert
も実行されるようになります。
// 良くない例02
it('ユーザーを保存する', done => {
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
done()
})
})
しかしこれだとまだよくないです。
#例外発生時にもdoneを呼ぶ
assert
が失敗すると例外が発生します。例外が発生すると、それ以降の処理は実行されず、Mochaがテストの失敗として処理してしまうので、done
が呼ばれないままテストが終了してしまいます。テストの失敗は検知されますが、実行結果が変な感じになります。
// 良いけどイマイチな例01
it('ユーザーを保存する', done => {
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
done()
}).catch(done)
})
Promise
のthen()
には処理が正しく終了したときに実行される処理を登録して、catch()
に例外が発生したときに実行される処理を登録します。catch(done)
って書いておくとassert
でエラーになったときもdone
が呼ばれるので、mochaがタイムアウトになるのを防ぐことができます。
User.create
でエラーが発生した場合も、このcatch
に登録した処理が呼ばれるので安心です。
ただ、done
を書き忘れると意図しない結果になりますし、このdone
自体はテストと関係のない記述なので、こういう記述が増えるとソースコードが読みにくくなってしまい、あまり良くないと思います。
テスト関数がPromiseを返すと、終わるまで待ってくれる
User.create
はPromise
を返す非同期の関数ですが、Promise.then
もまたPromise
を返す非同期の関数です。このPromiseオブジェクトをテスト関数の戻り値として返すと、MochaはこのPromiseの実行が完了するまでテストの終了を待ってくれるようになります。
なので、Promiseオブジェクトを返すようにすると、あっちこっちにdone
を書かなくてもよくなり、コードもキレイになります。
この時テスト関数の引数にdone
を指定したままだと、「doneが実行されるまでテストが終わらない」モードになってしまうので引数の数を0にするのを忘れないようにします。
// 良い例01
it('ユーザーを保存する', () => {
return User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
})
})
ネストしたthenだと最後のPromiseを返せない
テスト関数の戻り値としてPromise
を返そうとしたときに、then
がネストしていると内側のPromiseを返すことができません。
then
に渡した関数がPromise
を返すと、その戻り値のPromise
に対してthen
を続けて書くことができるので、こっちの書き方にします。
//よくない例B01
it('ユーザーを保存する', () => {
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
User.findOne({ where: { email: 'test@gmail.com' } })
.then(user => {
assert.equal(user.name, 'テストユーザー')
user.destroy() // <- このPromiseを一番外側の関数の戻り値にしたい
})
})
})
})
//よい例B01
it('ユーザーを保存する', () => {
return User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
return User.findOne({ where: { email: 'test@gmail.com' } })
})
.then(user => {
assert.equal(user.name, 'テストユーザー')
return user.destroy() // <- このPromiseが戻り値になる
})
})
#returnとか{}とか書きたくない
これは完全に個人的な好き嫌いの話なのですが、JavaScriptの無名関数は一文で終わる場合にreturn
と{
と}
を省略することができます。
const one = () => { return 1 }
👆これは、省略して👇のように書くことができます。
const one = () => 1
なので、良い例01は以下のように書き直すことができます。
// 良い例01
it('ユーザーを保存する', () => {
return User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
})
})
// 良い例01スッキリ版
it('ユーザーを保存する', () =>
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
assert.equal(user.name, 'テストユーザー')
})
)
良い例B01も書き直しておきます。
//よい例B01
it('ユーザーを保存する', () => {
return User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user => {
return User.findOne({ where: { email: 'test@gmail.com' } })
})
.then(user => {
assert.equal(user.name, 'テストユーザー')
return user.destroy() // <- このPromiseが戻り値になる
})
})
//よい例B01スッキリ版
it('ユーザーを保存する', () =>
User.create({
email: 'test@gmail.com',
name: 'テストユーザー'})
.then(user =>
User.findOne({ where: { email: 'test@gmail.com' } })
)
.then(user => {
assert.equal(user.name, 'テストユーザー')
return user.destroy() // <- このPromiseが戻り値になる
})
)
#フック関数にも同じ理屈が適用される
上に書いたことはフック関数のbefore
とかafter
にも適用されます。
after
関数でデータを削除して、削除が終わるまで次のテストに行くのを待って欲しいときは、以下のように書きます。
after(() =>
User.findOne({
where: { email: 'test@gmail.com' }
})
.then(user =>
user.destroy()
)
)
##複数行を消すとき
user.destroy()
もPromiseを返すので、Array#map
関数でPromiseの配列に変換することができます。Promiseの配列をPormise.all
に渡すと、「全てのPromiseが完了したら完了するPromise」を作ることができます。
after(() =>
User.findAll({
where: { email: 'test@gmail.com' }
})
.then(users =>
Promise.all(users.map(user => user.destroy()))
)
)
#DB接続を閉じる
そもそもなんでこんなことを必死に調べたかというと、PostgreSQLに接続してデータを登録したり検索したりするテストを書いたら、いつまでたってもテストが終了しなくて困ってたのです。
なんで終わらないのだろうと思って調べたら、どうもDB接続が残ってるので、ちゃんとclose
を呼んであげないと処理が終了できないらしい。
じゃあafter
で接続を閉じればいいのだろうと思い、以下のように書いたのですが、、、
after(() => {
sequelize.close()
})
after
に書いたから全てのテストが終わってから実行されるのだろうと思ってました。しかし非同期処理について何も知らない状態でよくわからないままテストを書いていたので、他のテストが完全に終わる前にafter
が実行されてしまい、「DB接続がないからテストが実行できないエラー」が多発するひどい状態になりました。
他のテストをちゃんと終わらせてからafter
が実行されるようにするために色々と調べて勉強になったので、忘れないように書いておきます。
まだ理解が足りてない気がするので、間違ってたらコメントとかで教えてください!お願いします!!