- 投稿日:2019-07-27T19:10:20+09:00
Laravel7 Voyagerで管理画面作成
VOYAGERの環境を構築
Composerをインストールする。
curl -sS https://getcomposer.org/installer | php mv composer.phar /usr/local/bin/composerVOYAGERのインストール
Voyagerパッケージを作成したプロジェクトに導入する。
$ composer require tcg/voyager–with-dummy オプションは、ダミーの管理者データの作成コマンド。
artisanコマンドで後から追加可能だが、インストール時にダミーデータを作成する。$ php artisan voyager:install --with-dummyVoyagerインストール先ディレクトリ
vender/tcg/voyagerセッションクッキーを管理画面とユーザで分ける
app/.envSESSION_COOKIE=auth SESSION_COOKIE_ADMIN=auth-adminconfig/app.php'providers' => [ /* * Laravel Framework Service Providers... */ TCG\Voyager\VoyagerServiceProvider::class, //追加する ],DB:MySQLの使用時
MySQLのバージョンによって、Laravelのutf8mb4_unicode_ciで不具合が発生する可能性がある。
AppServiceProvider.phpを修正しエラーを回避する。
デフォルトのdefaultStringLengthは255
となっている。app/Providers/AppServiceProvider.phpuse Illuminate\Support\Facades\Schema; // 追加 public function boot() { Schema::defaultStringLength( 191 ); // 追加 }管理者画面へログイン
ログイン画面へアクセス。
email :
admin@admin.com
password:password
アカウントの追加方法
$ php artisan voyager:admin admin@example.com --create //任意のメールアドレス Enter the admin name: > admin //任意の名前 Enter admin password: > Confirm Password: > Creating admin account The user now has full access to your site.Voyagerの日本語化
config/app.php'locale' => 'ja',ログイン後、右上のメニューから
Profile
→Edit My Profile
→Locale を ja
にセットする。
- 投稿日:2019-07-27T16:04:57+09:00
Laravel ide-helperでモデルのIDE保管用コメントを作成する
LaravelでIDE補完されやすくするではide-helperの導入とLaravelの基本クラスのIDE保管用ファイルの作成方法を紹介しました。
今回はide-helperを使用してEloquentクラスを継承したモデルのプロパティなどをテーブルから作成してくれる機能を紹介します。
使用するテーブルとモデルはLaravelでマイグレーションしてモデルファイルを作成するの記事でご紹介したshopsテーブル/Shopモデルを使用します。
doctrine/dbalのインストール
ide-helperでモデルにコメントを入れる機能を使用するにはdoctrine/dbalの追加が必要になるので下記コマンドでインストールします。
composer require --dev doctrine/dbal
コマンドの実行
インストールしたら下記コマンドを実行すると書くモデルファイルに対してIDE補完用のコメントが追加されます。
php artisan ide-helper:modelsコマンドを実行すると上書きしていいか聞かれるので、yesと入力してenterを押下します。
するとこのような何の記述もないモデルファイルが
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Shop extends Model { // }このようになります。
<?php namespace App; use Illuminate\Database\Eloquent\Model; /** * App\Shop * * @property int $id * @property string|null $name 店舗名 * @property string|null $sub_name 支店名 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop newQuery() * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop query() * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop whereId($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop whereName($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop whereSubName($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Shop whereUpdatedAt($value) * @mixin \Eloquent */ class Shop extends Model { // }もしプロジェクトルート/app配下以外にモデルファイルを作成している場合(例えばapp/Models/Shop.phpみたいな場合は)
php artisan ide-helper:models App\\Models\\Shopのようにnamespaceを指定してあげると反映されます。
- 投稿日:2019-07-27T09:38:47+09:00
Laravelでsqliteに画像をとりあえず投入
だってDBは使える容量が300MBなんですもの
https://github.com/JFLABO/LalaSQLite/blob/master/readme.md
- 投稿日:2019-07-27T05:55:30+09:00
たった3行 世界一カンタンにLaravelのファサードを作る方法(とサービスコンテナ的観点から掘るファサードの本質)
世界一カンタンなファサードの作り方(当社比)
Facadeをつくる手順は下記の通り。カッコは省略可能です。
- ファサード化したいクラスを作る
- ファサードクラスを作る
- (サービスプロバイダを作る)
- (サービスコンテナにインスタンス化する方法を伝える)
- (エイリアスを書く)
カッコは省略可能ですが、エイリアスは作っておきましょう。
というわけで今回の手順はこれだけ。
- クラスを作る
- ファサードクラスを作る
- (エイリアスを書く)
そして、これを実現するのに、書き換えるコードは3行。
あとはコピペです。クラスを作る
まずは、ファサード化したいクラスを作ります。
クラスを置く場所や中身は何でもいいのですが、
ファサードとして提供したいメソッドはすべて「インスタンスメソッド」で書くのがポイントです。ひとまず、 Services というネームスペースを用意して MyLoggerService を作ってみます。
app\Services\MyLoggerService.phpnamespace App\Services; class MyLoggerService { public function write( string $message ) { // ... } }当然ですが、このままではスタティック呼び出しはできません。
use App\Services\MyLoggerService; MyLoggerService::write('なにかログを記録するよ'); // Deprecated: Non-static method MyLoggerService::write() should not be called statically#改めてやってみたら Deprecated と警告されるだけで、できるにはできるのね……。PHPは優しいなぁ。
ファサードクラスを作る
以下をコピペしてください。
app/Facades/MyLogger.php<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; class MyLogger extends Facade // 【1行目】ここのクラス名と { protected static function getFacadeAccessor() { // Facade化したいクラスのクラス名を書く return \App\Services\MyLoggerService::class; // 【2行目】ここを書き換える } }ファサードは artisan コマンドでも作れますが、書換がたった2行なのでコピペしたほうが速いです。
作る場所はどこでもいいのですが、大きく2通りの考え方があって、
1つは app/Facades といった共通のディレクトリにまとめること。
もう1つが、ファサード化対象のクラスの近くに作ること。(Laravel標準のFacadeは、ほぼ前者。
Illuminate\Support\Facades にまとまっています。)サービスクラスの数や種類が少なければ前者がいいですが、
サービスクラスの種類が多かったりモジュールが別れていたりする場合は、
無理に1箇所に集めるより、対象クラスの近くに置くほうが管理が楽です。
その場合でも、どんなファサードがあるかは後述する「エイリアス」のところで一覧管理できますし。でも、今回はカンタンにしたいので、前者。
さて、こうすると、そのファサードクラスを使って、
さきほど作ったインスタンスメソッドを、
いきなりスタティックメソッド的に使うことができるようになります。use App\Facades\MyLogger; MyLogger::write('なにかログを記録するよ'); // 動く!これで ファサードの完成 です。
早い!※ここで登場した唯一のメソッド getFacadeAccessor は、解説コーナーで詳しく取り上げています。
エイリアスを作る
ちょっとちょっと!
Laravel標準のFacadeって、単語1つの短い名前でも使えるでしょ。
それ欲しい!ごもっともです。
ただ、それを実現しているのは Facade ではなく「エイリアス」という機能です。
#あるクラスが別のクラス名で定義されているかのように使える機能。別にFacadeじゃなくてもエイリアスは作れます。エイリアスの追加自体は超簡単で、はじめから用意されている
config/app.php
を開いてconfig/app.php'aliases' => [ 'App' => Illuminate\Support\Facades\App::class, 'Artisan' => Illuminate\Support\Facades\Artisan::class, // ... たくさんある 'View' => Illuminate\Support\Facades\View::class, // ここに追加 エイリアスクラス名 => 実態クラス名 のペア 'MyLogger' => App\Facades\MyLogger::class, // これが最後の【3行目】 ],とするだけ。こうすると、
use App\Facades\MyLogger; MyLogger::write('なにかログを記録するよ');これが
use MyLogger; MyLogger::write('なにかログを記録するよ'); // それか\をつけて use せずにいきなり使う。 \MyLogger::write('なにかログを記録するよ');このように使えるようになります。
僕自身、Laravel始めた頃に
use Illuminate\Support\Facades\DB; use DB;と2つのクラスがあって、どっちを使ったらいいか迷ったりしていましたが、
どちらも 全く同じもの です。Facade使ってるぜ!ということを明確にしたいなら前者のロング版(IDEのコードヒントがうまく動く)。
Facadeの「あとから差し替え可能だぜ!」というメリットを最大限活かしたいなら後者のショート版。
個人的には、VSCodeの自動補完が効くので、ロング版で書くことが多いです。解説
#「ファサード」というと建築用語で、建物の正面外観のデザインを指します。
ヨーロッパの街並みはレンガ造りの長屋が多く、
建物の正面デザインだけで個性を競う文化が発達したので専門用語があるんですね。
(昔は建築家といえばファサードデザイナーのことを指していたとか)Facadeとは何か?
それはさておき、LaravelのFacadeとは一体なんなのか?
Laravel Facadeの機能
- クラスのインスタンスメソッドがスタティックメソッドのように使える
- カンタンにモック化できる(…という機能もあるけどこの記事では触れません)
以上。
Facadeに対するよくある誤解
- えーと……要するに、スタティックメソッドを書くところですよね?
- DB というネームスペースを使わない短い名前のクラスが作れるからアクセスがカンタン♫
→ それは「エイリアス」- Laravelに用意されている DB とか Cookie とか Redis といったクラスのことだ!
→ ファサードには違いないけど、今回は標準ファサードではなく、独自ファサードの作り方(本質)でちょっと話題が違う。- それだけのために、サービスプロバイダ書いて、コンテナに登録して……手順が多くてめんどくさい!!
→ 3行でできましたよー。[一般的な話] サービス(クラス)とは?
ファサードの話をする前に、それと同時によく耳にする「サービス」とか「サービスクラス」とは何かという話を少しはさみます。
とは言っても、この「サービス」に明確な定義はありません。
とても抽象的。
ぶっちゃけ「何でもいい」くらいに言われることがほとんどです。
ただ、強いてアプリケーション開発における「サービス」というと、おぼろげながら下記のようなイメージで語られているようです。
- アプリケーションのとある機能についてまとめられたクラスやモジュール
- かなり具体的な機能のメソッドやインターフェースをいくつか持つf
- アプリケーションのどこからでもササッと呼び出される(ある機能を使うのに下準備がそれほど必要ない)
たとえば、「注文」というサービスに、「注文を作成する」「注文を確定する」「支払いする(支払い情報を紐付ける)」「発送する(発送情報を紐付ける)」といったメソッドがある……イメージ。
「ログ」というサービスには「ログを記録」「ログを一覧で取得する」といったメソッドがあるイメージ……。それで「どこからでもササッと」なら………。
スタティックメソッドで実装したらいい感じですよね!class Logger { public static function write( string $message ) { // ... } // ... } Logger::write('エラーが起きました');そうそう、まさにそんなイメージです。
[一般的な話] Facadeパターンとは?
LaravelのFacadeと同じ名前なのでややこしいですが、
それとは別の、デザインパターンの1つ「Facadeパターン」とはなにかをお話します。ファサードパターンは、
さきほど出てきた「サービスクラス」的なものを作るときの、
作り方の1つ(設計方法)で、
- サービスクラスが提供する機能にどんなものがあるか?だけ厳密に決めておく(メソッド名や引数、返り値など)
- 具体的な実装は隠して、あとから差し替えが効くようにする
という方法論のことです。
この「提供する機能だけ決まっている」というのが、
お店の外観はビシッとカッコよく決まっているけど、
中身はまだ改装中……みたいな
ファサードしか無い というのがネーミングの由来と思われます。
https://commons.wikimedia.org/wiki/File:Scenic_Design,_The_Family_Series,_by_Glenn_Davis.JPG
#なのでより正しいイメージとしては、建築のファサードというよりも、舞台セットのような ハリボテの立て看板 に近いです。これだけ聞くと、PHPではインターフェースがぴったり来るイメージですね。
interface LoggerInterface { public static function write( string $message ); // ... } class Logger implements LoggerInterface { public static function write( string $message ) { // 中身はまだ構築中... } // ... }さっきのスタティックメソッドはダメなんですか?
そう。ダメです。
スタティックメソッドは「あとから差し替え」が非常に困難です。
(しかもそれ自体がガッツリ実装を持っています。)
サービス的なものを作ることはできても、それは「Facadeパターンではない」ということになります。[Laravelの話] Facadeとは?
さて、サービスクラスとファサードパターンの2つの話が出てきましたが、
このうち、Laravel の Facadeは、
後者の「ファサードパターン」をPHPでカンタンに実現するための機能です。前者じゃないよ(スタティックメソッドを作るための機能じゃないよ)。ということ。
これ、自分も身に覚えのある「誤解」です。スタティックメソッドじゃだめな理由
Laravel標準のヘルパ関数をオーバーライドする話でも実践しましたが
グローバル関数や、スタティックメソッドは、
それをあとから置き換えることがめちゃくちゃ困難です。1回作っちゃうと、コンクリの基礎でガッチリ固められちゃっているように、動かせない。
これがアンチパターンとされている最大の理由です。でも、先程「サービスクラス」でも触れましたが、
サービス的なクラスを作ろうとすると スタティックメソッドを作りたくなってくる。そこで、アンチパターンを避けつつ、スタティックメソッドを作るための仕組みとして、
Laravelは、Facadeパターンをうまく取り入れた、
Facadeという機能を用意してくれているんです。Laravelがそれを解決する方法論
Facadeの動作原理はカンタンで、
- スタティックメソッド的な呼び出し方をされる
- (未定義なので)マジックメソッド __callstatic が呼ばれる
- getFacadeAccessor でコンテナのキーを得る
- サービスコンテナからインスタンスをもらう
- 呼ぶ
という感じ。実際のコードも下記のとおり、とてもシンプルです。
vendor/lavael/framework/src/Illuminate/Support/Facades.phppublic static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); }getFacadeAccessorの返り値
というわけで、 getFacadeAccessor の返り値を振り返ります。
標準のファサードを見ると 'app' とか 'db' といった短い文字列なのに、
なぜ今回、クラス名を使用したのでしょうか?先程ちらっと触れましたが、
実はここは「サービスコンテナにインスタンスをもらうためのキー名」を与えています。
「第3回 サービスコンテナ 結合編」で言うところの abstract 抽象 です。
つまり、$instance = app('app'); $instance = app('db'); $instance = app(\App\Services\MyLoggerService::class); // MyLoggerServiceのインスタンスが返るとしたときに、期待したインスタンスが返ってくるかどうか、ということ。
ここを、
$instance = app('mylogger'); // MyLoggerServiceのインスタンスが返ってきてほしいとしたければ、
app()->bind( 'mylogger', \App\Services\MyLoggerService::class );と結合を定義すればオッケー!というのは第3回の復習で、こうしておけば
protected static function getFacadeAccessor() { return 'mylogger'; }こうできます。
でも、クラス名だったら、bindしなくても最初から使えるよね、
だからキーはクラス名でいいよね、というのが「第1回 サービスコンテナ new編」 の復習。#ちなみに、getFacadeRoot はインスタンスをキャッシュする機能が備わっていたりするので、Facadeが使うインスタンスはすべて自動的にシングルトンになる、といった付加機能もあります(今気づきました笑)。つい下記のようにシングルトン結合を書きたくなってきますが、要らん!ということですね。
app()->singleton( \App\Services\MyLoggerService::class );まとめ
ファサードとはなにか?
LaravelのFacadeの本質をまとめるとこうです。
スタティックメソッドを書きたい!という欲求に、
スタティックメソッドを書かせずに応えるための仕組み。最終的に出来上がるのはスタティックメソッドですが、
スタティックメソッドを1つも書かない!というのが最大の特徴です。その機能の実現だったら3行でサクッとできますので、
スタティックメソッドが書きたい!と思ったら、
代わりにこのFacadeを使ってみてください。こんな記事も書いています
Laravelのちょっとマニアックな視点から、誰も書かない記事を書いています(笑
合わせてご覧いただけると幸いです(^^)今回と似た内容の「ヘルパ関数編」
サービスコンテナ講座
- 投稿日:2019-07-27T00:36:26+09:00
Laravel プロジェクトに Jestを入れて Typescript のテストを作成するデモ
目標:Laravel のプロジェクトに Jest を導入して Typescript をテストできるようになる。
今回のデモの目標は下記の通りです。
- Laravel プロジェクトに Jest をインストールする。
- Jest で Typescript のテストを作成して実行する。
デモ時のフレームワークのバージョンは下記の通りです。
- Laravel/framework: v5.8.29
- Typescript: 3.5.3
- Jest: 24.8.0
今回は実装の詳細は触れませんが、下記の日付カスタム書式変換機能をTypescript で実装し、
EnglishCustomDateFormatFunctionsMap
クラスとDateFormatConverter
クラスをテストを作成したいと思います。
https://github.com/ttn1129/laravel-jest-typescript-demo
0. デフォルトの Laravel プロジェクトを作成する。
下記の通り、デフォルトの Laravel プロジェクトを作成します。
デフォルトのLaravelプロジェクト作成.shlaravel new laravel-jest-typescript-demo cd laravel-jest-typescript-demo npm install1. Typescript と Jest のインストール(Typescript で使用できる設定)
Typescript と Jest をインストールします。
Laravel に Vue.js、Typescript を導入する際の設定については、下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
Laravel 5.5で簡単!Vue.js + TypeScript
https://www.hypertextcandy.com/laravel-vue-typescriptJest で Typescript を使用する際の設定については下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
TypeScriptでJestを使うときの設定(ts-jest, @types/jestなど)
https://dackdive.hateblo.jp/entry/2019/04/15/000000npm install -D jest ts-jest @types/jest ts-loader typescript vue-property-decorator併せて、下記の通り記述した jest.config.js をプロジェクトのルートディレクトリに追加します。
jest.config.jsmodule.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.jslet 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. テスト対象機能の実装
この実装での
CustomDateFormatFunctionsMap
クラスは key のカスタム日付フォーマットに対応するDateインスタンスの変換処理を持ちます。resources/ts/DateFormat/EnglishCustomDateFormatFunctionsMap.tsimport 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.tsimport 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.tsimport 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)', () => { expect(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'); }); });テスト結果.txtvagrant@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.全部のテストの実行時の結果.txtvagrant@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.(テストが通った時の達成感)
その他雑感
- 複雑な継承など処理が書ける。(そのため、javascriptを書いている感じが全くしない。色々なデザインパターンも実装できそう。)
以上です。
- 投稿日:2019-07-27T00:36:26+09:00
Laravel プロジェクトに Jestを入れて Typescript のテストを作成するデモ(カスタム日付フォーマット機能の実装)
目標:Laravel のプロジェクトに Jest を導入して Typescript をテストできるようになる。
今回のデモの目標は下記の通りです。
- Laravel プロジェクトに Jest をインストールする。
- Jest で Typescript のテストを作成して実行する。
デモ時のフレームワークのバージョンは下記の通りです。
- Laravel/framework: v5.8.29
- Typescript: 3.5.3
- Jest: 24.8.0
今回は実装の詳細は触れませんが、下記の日付カスタム書式変換機能をTypescript で実装し、
EnglishCustomDateFormatFunctionsMap
クラスとDateFormatConverter
クラスをテストを作成したいと思います。
0. デフォルトの Laravel プロジェクトを作成する。
下記の通り、デフォルトの Laravel プロジェクトを作成します。
デフォルトのLaravelプロジェクト作成.shlaravel new laravel-jest-typescript-demo cd laravel-jest-typescript-demo npm install1. Typescript と Jest のインストール(Typescript で使用できる設定)
Typescript と Jest をインストールします。
Laravel に Vue.js、Typescript を導入する際の設定については、下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
Laravel 5.5で簡単!Vue.js + TypeScript
https://www.hypertextcandy.com/laravel-vue-typescriptJest で Typescript を使用する際の設定については下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
TypeScriptでJestを使うときの設定(ts-jest, @types/jestなど)
https://dackdive.hateblo.jp/entry/2019/04/15/000000npm install -D jest ts-jest @types/jest ts-loader typescript vue-property-decorator併せて、下記の通り記述した jest.config.js をプロジェクトのルートディレクトリに追加します。
jest.config.jsmodule.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.jslet 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. テスト対象機能の実装
この実装での
CustomDateFormatFunctionsMap
クラスは key のカスタム日付フォーマットに対応するDateインスタンスの変換処理を持ちます。resources/ts/DateFormat/EnglishCustomDateFormatFunctionsMap.tsimport 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.tsimport 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.tsimport 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)', () => { expect(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'); }); });テスト結果.txtvagrant@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.全部のテストの実行時の結果.txtvagrant@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その他雑感
- 複雑な継承など処理が書ける。(そのため、javascriptを書いている感じが全くしない。色々なデザインパターンも実装できそう。)
以上です。
- 投稿日:2019-07-27T00:36:26+09:00
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プロジェクト作成.shlaravel new laravel-jest-typescript-demo cd laravel-jest-typescript-demo npm install1. Typescript と Jest のインストール(Typescript で使用できる設定)
Typescript と Jest をインストールします。
npm install -D jest ts-jest @types/jest ts-loader typescript vue-property-decorator併せて、下記の通り記述した jest.config.js をプロジェクトのルートディレクトリに追加します。
jest.config.jsmodule.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.jslet 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
クラスをテストを作成したいと思います。
この実装での
CustomDateFormatFunctionsMap
クラスは key のカスタム日付フォーマットに対応するDateインスタンスの変換処理を持ちます。resources/ts/DateFormat/EnglishCustomDateFormatFunctionsMap.tsimport 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.tsimport 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.tsimport 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'); }); });テスト結果.txtvagrant@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.tsimport 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日(月曜日)'); }); });全部のテストの実行時の結果.txtvagrant@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-typescriptJest で Typescript を使用する際の設定については下記のサイトに詳しく書いて頂いておりましたので参考にさせて頂きました。
TypeScriptでJestを使うときの設定(ts-jest, @types/jestなど)
https://dackdive.hateblo.jp/entry/2019/04/15/000000その他雑感
- 複雑な継承など処理が書ける。(そのため、javascriptを書いている感じが全くしない。色々なデザインパターンも実装できそう。)
そもそもjavascript のDate にカスタム日付書式変換機能が無いという事実に頭がオーバーフローした結果、色々ぼやぼやと調べながら考えつくままに機能をtypescript で作ったのとJestでテストするのもやってみたので、今週は毎日頭がパンパンでしたが、新しく学ぶことだらけでよかったです。
以上です。