jestとかjasmineでmomentを含むオブジェクトの曖昧なマッチャーを書く

# バージョン情報
- "jasmine-core": "~3.4.0"
- "karma": "~4.1.0"
- "jest": "^24.9.0"

(フロントエンドはangular、バックエンドは普通のtypescriptで開発しているため、フロントエンドのテストはkarma/jasmine、バックエンドのテストはjestで行っている構成)

# やりたいこと

- 日付を返すUtilとかの単体テスト

```typescript

import * as moment from 'moment';

export namespace DateUtil {
  export const nextDay(m:moment.Moment) => {
    return moment(m).add(1,'days');
  }
}

```

例えばこういうUtil（本当はもっとやる気があるが）があったとして、こういうテストを書きたいとする。

```typescript
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でも似たような状況になる

```console
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.
```

まあ、言いたいことはわかる。
インスタンス化の過程の情報を持っているようなイメージだろうか。

ちなみにこれはうまくいく。

```typescript
// 同様の経路でインスタンス化された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


```typescript

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をマッチ結果としてみなすという構造になっている。

これでやってみると通るようになる。
最終的なコードは以下の通り。

```typescript
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()したものが表示されるっぽい気がする。

```console
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を実装するときれいなメッセージになる。