Laravel プロジェクトに Jestを入れて Typescript のテストを作成するデモ（カスタム日付書式変換機能の実装）

## 目標：Laravel のプロジェクトに Jest を導入して Typescript をテストできるようになる。

今回のデモの目標は下記の通りです。

- Laravel プロジェクトに Jest をインストールする。
- Jest で Typescript のテストを作成して実行する。

デモ時のフレームワークのバージョンは下記の通りです。

- Laravel/framework: v5.8.29
- Typescript: 3.5.3
- Jest: 24.8.0

## 0. デフォルトの Laravel プロジェクトを作成する。
下記の通り、デフォルトの Laravel プロジェクトを作成します。


```デフォルトのLaravelプロジェクト作成.sh
laravel new laravel-jest-typescript-demo
cd laravel-jest-typescript-demo
npm install
```

## 1. Typescript と Jest のインストール（Typescript で使用できる設定）
Typescript と Jest をインストールします。

```sh
npm install -D jest ts-jest @types/jest ts-loader typescript vue-property-decorator
```

併せて、下記の通り記述した jest.config.js をプロジェクトのルートディレクトリに追加します。

```jest.config.js
module.exports = {
  testRegex: 'resources/ts/tests/.*.spec.ts$',
  preset: 'ts-jest'
}
```

また、package.json、tsconfig.json と webpack.mix.js を下記の通り作成します。

```package.json
{
    "scripts": {
        //中略
        "test": "jest"
    },
}

```

```tsconfig.json
{
  "compilerOptions": {
    "outDir": "./built/",
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "module": "es2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "target": "es5",
    "lib": [
      "es2016",
      "dom"
    ]
  },
  "include": [
    "resources/ts/**/*"
  ]
}
```

```webpack.mix.js
let mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.ts('resources/ts/app.ts', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

```

これでJest による Typescript のテストの環境設定が完了しました。

## 2. テスト対象機能の実装

今回は実装の詳細は触れませんが、下記の日付カスタム書式変換機能をTypescript で実装し、 `EnglishCustomDateFormatFunctionsMap`クラスと `DateFormatConverter` クラスをテストを作成したいと思います。
![DateFormatConverter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342307/cd567fc7-bce8-5076-d3e1-60b1999a563e.png)



この実装での `CustomDateFormatFunctionsMap` クラスは key のカスタム日付フォーマットに対応するDateインスタンスの変換処理を持ちます。

```resources/ts/DateFormat/EnglishCustomDateFormatFunctionsMap.ts
import CustomDateFormatFunctionsMapInterface from './CustomDateFormatFunctionsMapInterface';
/**
 * 日付のカスタム書式種類毎の英語ロケールでの変換関数を保持するクラス
 */
export default class EnglishCustomDateFormatFunctionsMap implements CustomDateFormatFunctionsMapInterface {

  //HH	24時間単位	09
  HH (date:Date):string { return ('0' + date.getHours().toString()).slice(-2); }
  //H	24時間単位（先頭の0なし）	9
  H (date:Date):string { return date.getHours().toString(); }

  //mm	分（2桁）	09
  mm (date:Date):string { return ('0' + date.getMinutes()).slice(-2); }
  //m	分（1桁）	9
  m (date:Date):string { return date.getMinutes().toString(); }
  //ss	秒（2桁）	09
  ss (date:Date):string { return ('0' + date.getSeconds()).slice(-2); }
  //s	秒（1桁）	9
  s (date:Date):string { return date.getSeconds().toString(); }
  //dd	日（2桁）	09
  dd (date:Date):string { return ('0' + date.getDate()).slice(-2); }
  //d	日（先頭の0なし）	9
  d (date:Date):string { return date.getDate().toString(); }
  // yyyy	西暦（4桁）	2015
  yyyy (date:Date):string { return date.getFullYear().toString(); }
  // yy	西暦（2桁）	15
  yy (date:Date):string { return (date.getFullYear().toString()).slice(2); }

  // dddd	曜日・週	英語	Tuesday
  dddd (date:Date):string {return ["Sunday", "$onday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()]; }
  
  // ddd	曜日・週	英語（略語）	Tue
  ddd (date:Date):string {return ["Sun", "$on", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()]; }
  // MMMM	英語	july
  MMMM	 (date:Date):string { return ["January", "February", "$arch", "April", "$ay", "June", "July", "August", "September", "October", "November", "December"][date.getMonth()]; }
  // MMM	英語（略語）	jul
  MMM (date:Date):string {return ["Jan", "Feb", "$ar", "Apr", "$ay", "Jun", "Jly", "Aug", "Spt", "Oct", "Nov", "Dec"][date.getMonth()]; }
  // MM	月（2桁）	07
  MM (date:Date):string { return ('0' + (date.getMonth() + 1)).slice(-2); }
  // M	月（先頭の0なし）	7
  M (date:Date):string { return (date.getMonth() + 1).toString(); }
  $ (date:Date):string { return 'M'}
}
```

この `CustomDateFormatFunctionsMap`インスタンスを`DateFormatConverter` が保持して、メソッド`convertDate`内で呼び出してのすべてのカスタム書式の変換処理を渡された書式に対して実行します。

```resources/ts/DateFormat/DateFormatConverter.ts
import CustomDateFormatFunctionsMapInterface from './CustomDateFormatFunctionsMapInterface'
import CustomDateFormatFunctionsMapFactory from './CustomDateFormatFunctionsMapFactory'
import IndexableInterface from './IndexableInterface'
export default class DateFormatConverter {
  /**
   * 日付のカスタム書式種類毎の変換関数を保持するクラスインスタンス
   */
  private CustomDateFormatFunctionsMap:CustomDateFormatFunctionsMapInterface;

  /**
   * 日付のカスタム書式毎の関数を保持するクラスインスタンスを初期化する
   */
  constructor(locale:string | null = 'ja_jp'){
    this.CustomDateFormatFunctionsMap = CustomDateFormatFunctionsMapFactory.make(locale);
  }

  /**
   * 日付をカスタム書式に変換する
   */
  public convertDate(date:Date, format:string, locale:string | null = null):string {
    if(locale)
    {
      this.CustomDateFormatFunctionsMap = CustomDateFormatFunctionsMapFactory.make(locale);
    }
    return Object.keys(this.CustomDateFormatFunctionsMap).reduce(
      (res, customFormatString) => res.replace(customFormatString, this.callFunctionWithFormatString(customFormatString, date)), format
    );
  }

  /**
   * Formatに対応するカスタム日付書式変換処理を呼び出す
   */
  private callCustomFormatFunctionWithFormatString(customFormatString:string, date:Date):string
  {
    if (typeof (this.CustomDateFormatFunctionsMap as IndexableInterface)[customFormatString] === 'function')
    {
      return ((this.CustomDateFormatFunctionsMap as IndexableInterface)[customFormatString])(date);
    }
    return customFormatString;
  }
}
```

## 3. テストの作成

テストを下記の通り実装します。

```resources/ts/tests/EnglishCustomDateFormatFunctionsMap.spec.ts
import EnglishCustomDateFormatFunctionsMap from './../DateFormat/EnglishCustomDateFormatFunctionsMap';

describe('英語カスタム日付書式変換テスト', () => {
  const englishCustomDateFormatFunctionsMap = new EnglishCustomDateFormatFunctionsMap();
  const dummyDate_20190909_0909_1111:Date = new Date(2019, 8, 9, 9, 9, 11,11);
  const dummyDate_19201231_2301_1234:Date = new Date(1920,11,31, 23,1, 12,34);
  it('should be 2019', () => {
    expect(englishCustomDateFormatFunctionsMap.yyyy(dummyDate_20190909_0909_1111)).toEqual('2019');
  });
  it('should be 1920', () => {
    expect(englishCustomDateFormatFunctionsMap.yyyy(dummyDate_19201231_2301_1234)).toEqual('1920');
  });
  it('should be 19 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.yy(dummyDate_20190909_0909_1111)).toEqual('19');
  });
  it('should be 20 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.yy(dummyDate_19201231_2301_1234)).toEqual('20');
  });
  it('should be Spt (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.MMM(dummyDate_20190909_0909_1111)).toEqual('Spt');
  });
  it('should be Dec (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.MMM(dummyDate_19201231_2301_1234)).toEqual('Dec');
  });
  it('should be $ar', () => {
    expect(englishCustomDateFormatFunctionsMap.MMM(new Date(2019, 2, 9, 9, 19, 11,11))).toEqual('$ar');
  });
  it('should be September (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.MMMM(dummyDate_20190909_0909_1111)).toEqual('September');
  });
  it('should be December (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.MMMM(dummyDate_19201231_2301_1234)).toEqual('December');
  });
  it('should be $arch', () => {
    expect(englishCustomDateFormatFunctionsMap.MMMM(new Date(2019, 2, 9, 9, 19, 11,11))).toEqual('$arch');
  });
  it('should be 09 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.MM(dummyDate_20190909_0909_1111)).toEqual('09');
  });
  it('should be 12 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.MM(dummyDate_19201231_2301_1234)).toEqual('12');
  });
  it('should be 9 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.M(dummyDate_20190909_0909_1111)).toEqual('9');
  });
  it('should be 12 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.M(dummyDate_19201231_2301_1234)).toEqual('12');
  });
  it('should be 09 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.dd(dummyDate_20190909_0909_1111)).toEqual('09');
  });
  it('should be 31 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.dd(dummyDate_19201231_2301_1234)).toEqual('31');
  });
  it('should be 9 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.d(dummyDate_20190909_0909_1111)).toEqual('9');
  });
  it('should be 31 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.d(dummyDate_19201231_2301_1234)).toEqual('31');
  });
  it('should be $on (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.ddd(dummyDate_20190909_0909_1111)).toEqual('$on');
  });
  it('should be Fri (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.ddd(dummyDate_19201231_2301_1234)).toEqual('Fri');
  });
  it('should be $onday (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.dddd(dummyDate_20190909_0909_1111)).toEqual('$onday');
  });
  it('should be Friday (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.dddd(dummyDate_19201231_2301_1234)).toEqual('Friday');
  });
  it('should be 09 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.HH(dummyDate_20190909_0909_1111)).toEqual('09');
  });
  it('should be 23 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.HH(dummyDate_19201231_2301_1234)).toEqual('23');
  });
  it('should be 9 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.H(dummyDate_20190909_0909_1111)).toEqual('9');
  });
  it('should be 23 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.H(dummyDate_19201231_2301_1234)).toEqual('23');
  });
  it('should be 09 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.mm(dummyDate_20190909_0909_1111)).toEqual('09');
  });
  it('should be 01 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.mm(dummyDate_19201231_2301_1234)).toEqual('01');
  });
  it('should be 9 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.m(dummyDate_20190909_0909_1111)).toEqual('9');
  });
  it('should be 1 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.m(dummyDate_19201231_2301_1234)).toEqual('1');
  });
  it('should be 11 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.ss(dummyDate_20190909_0909_1111)).toEqual('11');
  });

  it('should be 01 and 1', () => {
    expect(englishCustomDateFormatFunctionsMap.ss(new Date(2019,1,1,1,1,1))).toEqual('01');
    expect(englishCustomDateFormatFunctionsMap.s(new Date(2019,1,1,1,1,1))).toEqual('1');
  });

  it('should be 12 (of 19201231_2301_1234)', () => {
    (englishCustomDateFormatFunctionsMap.ss(dummyDate_19201231_2301_1234)).toEqual('12');
  });
  it('should be 11 (of 20190909_0909_1111)', () => {
    expect(englishCustomDateFormatFunctionsMap.s(dummyDate_20190909_0909_1111)).toEqual('11');
  });
  it('should be 12 (of 19201231_2301_1234)', () => {
    expect(englishCustomDateFormatFunctionsMap.s(dummyDate_19201231_2301_1234)).toEqual('12');
  });

});
```

```テスト結果.txt
vagrant@homestead:~/code/laravel-jest-typescript-demo$ npm test

> @ test /home/vagrant/code/laravel-jest-typescript-demo
> jest

 PASS  resources/ts/tests/EnglishCustomDateFormatFunctionsMap.spec.ts (88.429s)
  英語カスタム日付書式変換テスト
    ✓ should be 2019 (13ms)
    ✓ should be 1920 (1ms)
    ✓ should be 19 (of 20190909_0909_1111) (1ms)
    ✓ should be 20 (of 19201231_2301_1234) (2ms)
    ✓ should be Spt (of 20190909_0909_1111) (1ms)
    ✓ should be Dec (of 19201231_2301_1234) (2ms)
    ✓ should be $ar (1ms)
    ✓ should be September (of 20190909_0909_1111) (2ms)
    ✓ should be December (of 19201231_2301_1234) (1ms)
    ✓ should be $arch (1ms)
    ✓ should be 09 (of 20190909_0909_1111) (2ms)
    ✓ should be 12 (of 19201231_2301_1234) (1ms)
    ✓ should be 9 (of 20190909_0909_1111) (1ms)
    ✓ should be 12 (of 19201231_2301_1234) (1ms)
    ✓ should be 09 (of 20190909_0909_1111) (1ms)
    ✓ should be 31 (of 19201231_2301_1234) (1ms)
    ✓ should be 9 (of 20190909_0909_1111) (1ms)
    ✓ should be 31 (of 19201231_2301_1234) (1ms)
    ✓ should be $on (of 20190909_0909_1111) (2ms)
    ✓ should be Fri (of 19201231_2301_1234) (1ms)
    ✓ should be $onday (of 20190909_0909_1111) (1ms)
    ✓ should be Friday (of 19201231_2301_1234) (1ms)
    ✓ should be 09 (of 20190909_0909_1111) (1ms)
    ✓ should be 23 (of 19201231_2301_1234) (1ms)
    ✓ should be 9 (of 20190909_0909_1111) (1ms)
    ✓ should be 23 (of 19201231_2301_1234) (1ms)
    ✓ should be 09 (of 20190909_0909_1111) (1ms)
    ✓ should be 01 (of 19201231_2301_1234) (8ms)
    ✓ should be 9 (of 20190909_0909_1111) (1ms)
    ✓ should be 1 (of 19201231_2301_1234) (1ms)
    ✓ should be 11 (of 20190909_0909_1111) (1ms)
    ✓ should be 01 and 1 (2ms)
    ✓ should be 12 (of 19201231_2301_1234) (1ms)
    ✓ should be 11 (of 20190909_0909_1111) (1ms)
    ✓ should be 12 (of 19201231_2301_1234) (1ms)

Test Suites: 1 passed, 1 total
Tests:       35 passed, 35 total
Snapshots:   0 total
Time:        106.931s
Ran all test suites.

```

`DateFormatConverter`のテストは下記の通りで実装しました。

```resources/ts/tests/DateFormatConverter.spec.ts
import DateFormatConverter from './../DateFormat/DateFormatConverter';

describe('カスタム日付書式変換テスト', () => {
  const dateFormatConverter = new DateFormatConverter('ja');
  const dummyDate_20190909_0909_1111:Date = new Date(2019, 8, 9, 9, 9, 11,11);
  const dummyDate_19201231_2301_1234:Date = new Date(1920,11,31, 23,1, 12,34);
  it('should be 1920 (of 19201231_2301_1234)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_19201231_2301_1234, 'yyyy')).toEqual('1920');
  });
  it('should be 19201231 (of 19201231_2301_1234)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_19201231_2301_1234, 'yyyyMMdd')).toEqual('19201231');
  });
  it('should be 1920年12月31日（金曜日） (of 19201231_2301_1234)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_19201231_2301_1234, 'yyyy年M月d日（dddd）')).toEqual('1920年12月31日（金曜日）');
  });
  it('should be 2019 (of 20190909_0909_1111)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_20190909_0909_1111, 'yyyy')).toEqual('2019');
  });
  it('should be 20190909 (of 20190909_0909_1111)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_20190909_0909_1111, 'yyyyMMdd')).toEqual('20190909');
  });
  it('should be 2019年9月9日（月曜日） (of 20190909_0909_1111)', () => {
    expect(dateFormatConverter.convertDate(dummyDate_20190909_0909_1111, 'yyyy年M月d日（dddd）')).toEqual('2019年9月9日（月曜日）');
  });
});

```


```全部のテストの実行時の結果.txt
vagrant@homestead:~/code/laravel-jest-typescript-demo$ npm test

> @ test /home/vagrant/code/laravel-jest-typescript-demo
> jest

ts-jest[config] (WARN) TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.
 PASS  resources/ts/tests/DemoGetPropertiesNameOfClass.spec.ts (28.88s)
 PASS  resources/ts/tests/DateFormatConverter.spec.ts
 PASS  resources/ts/tests/EnglishCustomDateFormatFunctionsMap.spec.ts
 PASS  resources/ts/tests/CustomDateFormatFunctionsMapFactory.spec.ts
 PASS  resources/ts/tests/JapaneseCustomDateFormatFunctionsMap.spec.ts

Test Suites: 5 passed, 5 total
Tests:       58 passed, 58 total
Snapshots:   0 total
Time:        53.087s
Ran all test suites.

```


ソースコード
https://github.com/ttn1129/laravel-jest-typescript-demo

## 参照

Laravel に Vue.js、Typescript を導入する際の設定については、下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
[Laravel 5.5で簡単！Vue.js + TypeScript
 https://www.hypertextcandy.com/laravel-vue-typescript](https://www.hypertextcandy.com/laravel-vue-typescript)

Jest で Typescript を使用する際の設定については下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
[TypeScriptでJestを使うときの設定（ts-jest, @types/jestなど） 
https://dackdive.hateblo.jp/entry/2019/04/15/000000](https://dackdive.hateblo.jp/entry/2019/04/15/000000)



## その他雑感
- 複雑な継承など処理が書ける。（そのため、javascriptを書いている感じが全くしない。色々なデザインパターンも実装できそう。）

そもそもjavascript のDate にカスタム日付書式変換機能が無いという事実に頭がオーバーフローした結果、色々ぼやぼやと調べながら考えつくままに機能をtypescript で作ったのとJestでテストするのもやってみたので、今週は毎日頭がパンパンでしたが、新しく学ぶことだらけでよかったです。

以上です。