バージョン情報
- "jasmine-core": "~3.4.0"
- "karma": "~4.1.0"
- "jest": "^24.9.0"
(フロントエンドはangular、バックエンドは普通のtypescriptで開発しているため、フロントエンドのテストはkarma/jasmine、バックエンドのテストはjestで行っている構成)
やりたいこと
- 日付を返すUtilとかの単体テスト
import * as moment from 'moment';
export namespace DateUtil {
export const nextDay(m:moment.Moment) => {
return moment(m).add(1,'days');
}
}
例えばこういうUtil(本当はもっとやる気があるが)があったとして、こういうテストを書きたいとする。
import { DateUtil } from './date-util.ts';
describe(`DateUtil#nextDay`,()=>{
it(`should return the next day when passed 大晦日`,()=>{
expect(DateUtil.nextDay(moment('2018-12-31','YYYY-MM-DD')).toEqual(moment('2019-01-01','YYYY-MM-DD'));
});
});
問題
普通にテストすると、こんな感じで怒られが発生する。
エラーメッセージはkarma/jasmineの場合だが、jestでも似たような状況になる
Chrome 77.0.3865 (Windows 10.0.0) DateUtil#nextDay should return the next day when passed 大晦日 FAILED
Error: Expected $._i = '2018-12-31' to equal '2019-01-01'.
Expected $._pf.parsedDateParts[0] = 2018 to equal 2019.
Expected $._pf.parsedDateParts[1] = 11 to equal 0.
Expected $._pf.parsedDateParts[2] = 31 to equal 1.
Expected $._a[0] = 2018 to equal 2019.
Expected $._a[1] = 11 to equal 0.
Expected $._a[2] = 31 to equal 1.
まあ、言いたいことはわかる。
インスタンス化の過程の情報を持っているようなイメージだろうか。
ちなみにこれはうまくいく。
// 同様の経路でインスタンス化されたmoment.Momentのインスタンスの比較
expect(moment('2019-01-01','YYYY-MM-DD')).toEqual(moment('2019-01-01','YYYY-MM-DD'));
// 違う経路でインスタンス化されたmoment.MomentのISOString自体の比較
expect(DateUtil.nextDay(moment('2018-12-31','YYYY-MM-DD').toISOString()).toEqual(moment('2019-01-01','YYYY-MM-DD').toISOString());
つらさ
toISOString()とかvalueOf()して比較するというのも手だが、これが構造化されたデータだったりするとつらい。
なるべくなら、jasmineやjestの機能にうまく乗る形で表現したい。
解決案
jasmineにはAsymmetric Matcherというものがある。
有名なのはjesmine.objectContainingとかで、要するに「厳密な二つのオブジェクトの完全一致ではなく、特定の条件を満たした場合にマッチ成功とみなす」ことを可能にしてくれる。
今回はこれを自作することにする。
参考:https://jasmine.github.io/2.2/introduction#section-Custom_asymmetric_equality_tester
export class MomentContaining {
constructor(private sample:moment.Moment){}
asymmetricMatch(other:any){
// ここらへんは適当。ほかに比較したいフィールドや振る舞いがあれば追加
if(!other.isValid || !other.isValid() || !other.toISOString)){
return false;
}
return other.toISOString() === this.sample.toISOString();
}
}
export const momentContaining = (sample:moment.Moment) => new MomentContaining(sample);
ちなみにjestのソースをあたると以下のように書いてある。
https://github.com/facebook/jest/blob/master/packages/expect/src/asymmetricMatchers.ts
呼び出されている箇所は以下の通り。
https://github.com/facebook/jest/blob/master/packages/expect/src/jasmineUtils.ts#L42
要はasymmetricMatchという関数を持っているオブジェクトが比較する二つのもののうちどちらかにあった場合、それを呼び出した結果のbooleanをマッチ結果としてみなすという構造になっている。
これでやってみると通るようになる。
最終的なコードは以下の通り。
import { DateUtil } from './date-util.ts';
export class MomentContaining {
constructor(private sample:moment.Moment){}
asymmetricMatch(other:any){
if(!other.isValid || !other.isValid() || !other.toISOString)){
return false;
}
return other.toISOString() === this.sample.toISOString();
}
}
export const momentContaining = (sample:moment.Moment) => new MomentContaining(sample);
describe(`DateUtil#nextDay`,()=>{
it(`should return the next day when passed 大晦日`,()=>{
expect(DateUtil.nextDay(moment('2018-12-31','YYYY-MM-DD')).toEqual(momentContaining(moment('2019-01-01','YYYY-MM-DD')));
});
});
当然だが、momentContainingに渡す日付を変えてみるときちんと失敗してくれる。
ちなみに、失敗したときはメッセージにMomentContainingのtoString()したものが表示されるっぽい気がする。
Chrome 77.0.3865 (Windows 10.0.0) DateUtil#nextDay should return the next day when passed 大晦日 FAILED
Error: Expected Tue Jan 01 2019 00:00:00 GMT+0900 to equal MomentContaining({ sample: Wed Jan 02 2019 00:00:00 GMT+0900 }).
なのでtoStringを実装するときれいなメッセージになる。