バイクとプログラミング

Category: TypeScript

10分でできるテスト自動化 JS / TS + GitHub Actions + Morse Unit Testで快適CI

Twitterで紹介しました自作のテストランナー、Morse-Unit-Test(モールス ユニットテスト)ですが、これとGitHub Actionsでうまいことテストが自動化できたので紹介したいと思います。

Morseのテスト結果出力

Morseは簡単シンプルなテストランナーです。テストの定義に複雑なドキュメントを読む必要がなく、必要最小限のテスト環境を提供しているため、小規模なプロジェクトに最適な仕様になっています。

さらに詳しく知りたい方はREADMEを参照してみてください。
morse-unit-test/README-JA.md at develop · kznrluk/morse-unit-test

Morseは人間にとって扱いやすいテストランナーを目指していますが、20KB以下のパッケージサイズ、依存ゼロ、テストの並列実行などなどの性能があり、CIと組み合わせてもストレスなく動作します。

設定レスなので、事前にテストだけ動かせる状態であれば、10分程度でGitHub Actionsと連携させることができるので、今回はその手順の紹介をします。

Step1: ローカルでテストを動かせるようにしておく

なにはともあれMorseをインストールします。

> npm install --save-dev morse-unit-test

インストールが完了したら早速テストを書きます。Morseは適当なファイルにテストを書き、その中でdoTest()関数を呼び出すだけで実行されます。この辺りは別途word-pokerのテストコードやドキュメントを参照してください。

word-poker/morse.ts at master · kznrluk/word-poker

テストが書き終わったらpackage.jsonを編集して、npm testでテストを実行できるようにしておきます。nodeでテストファイルを実行するだけでOKです。

"scripts": {
  "build": "tsc", // JSであれば不要
  "test": "npm run build && node ./build/tests/morse.js"
}

npm testでMorseが走ることを確認したらコミットしてリポジトリにpushします。

Step2: .github/workfrows/nodejs.yml の追加

この作業はブラウザで完結します。リポジトリのページを開いて、NodeJSをクリックします。

Node.jsの’Set up this workflow’を選択する
node-version以外は変える必要ありません

Morseは内部でArray#Flatを使っているので、テスト環境はNode12以上が必須になります。そのため、node-versionを12.xのみに編集してください。この仕様は後でなんとかします。

- node-version: [8.x, 10.x, 12.x]
+ node-version: [12.x]

編集したらコミットして保存します。

これで全ての設定が完了しました!あとは更新をpushするだけでGitHub Actionsで自動的にMorseがテストを行います。

Step3: テスト結果を見る

テスト結果はGitHub Actionsの画面から確認することができます。テストが通ると下のように表示されます。ちゃんとMorseのログも出ています。

テストが成功した場合

テストが失敗している場合は下のような表示になります。GitHub Actionsはコンソール色もそのまま出してくれるのでちゃんと見やすく表示できていますね。

テストが失敗した場合

もちろんプルリクエストの画面でもテスト結果が確認できます。

PR画面でテストに失敗していることがわかる

と、このような感じで簡単に導入することができました。

Morseはすぐ使えるテストランナーです。個人用リポジトリでも簡単に導入することができます。この機会にぜひMorseを使ってユニットテストを始めてみませんか?

TypeScriptで型安全なEventEmitterを実現する

Nodejsでイベント駆動プログラミングをするときに便利なのがevent.EventEmitterですが、これをTypeScriptで利用しようとするとうまく型安全にできません。

const event = new EventEmitter();
event.on('foo', arg => console.log(arg)); 
// argは必然的にanyとなる...
event.emit('foo', 123);
event.emit('foo', '123');
event.emit('foo', {}); // なんでもあり

この問題に対する解決策は以前からいくつか存在していましたが、どの方法もデメリットがある状態でした。

しかし、TypeScript 3.0から導入されたConditional Typesを利用することによりほぼ完全なイベントの型付けを実現できるようになりました。

今回は、それをライブラリとして利用しやすくしたstrict-event-emitter-typesを利用したので、忘備録として紹介します。

簡単な使用方法

大体README.mdに書かれている内容と同じです。

事前の準備として、イベントの型情報をまとめたインターフェースと、EventEmitterを取得します。


import StrictEventEmitter from 'strict-event-emitter-types';

// イベントの定義
interface Events {
  request: (request: Request, response: Response) => void;
  data: String; // (data: String) => void; のショートハンド
  done: void; // 引数なし
}

import { EventEmitter } from 'events';

// イベントエミッタとイベント定義を元にStrictEventEmitterを作成
const ee: StrictEventEmitter = new EventEmitter();

この方法で生成されたEventEmitter(サンプル内ee)は、全て型安全で利用することができます。

利用方法は普段のEventEmitterと同様です。


ee.on('request', (req, res) => ... );
// reqはRequest, resはResponseとして認識される。

ee.on('something', () => ... );
// イベントが存在しないのでコンパイルエラー

ee.on('done', hoge => ... );
// doneには引数がないのでコンパイルエラー

ee.emit('data', 'こんにちは!');
// OK

ee.emit('data', 12345);
// 型が合わないのでコンパイルエラー

簡単ですね。

ただし、TypeScriptの動作により、イベント定義インターフェースに不具合があった場合に不明瞭なエラーメッセージが出現します。

“not assignable to parameter of type ‘unique symbol'” error · Issue #9 · bterlson/strict-event-emitter-types

これはTypeScript側で修正予定だそうです。

Assignability of callback parameter defined with conditional tuple type · Issue #26013 · microsoft/TypeScript

strict-event-emitter-typesをラップする方法について

プルリクエストを送ってある件ですが、元ソース中のListenerTypeがExportされていないため、このライブラリのラッパを作成することができません。

Export ListenerType by kznrluk · Pull Request #13 · bterlson/strict-event-emitter-types

ローカルで編集してしまうか、私のPR元ブランチをクローンして使ってください。

kznrluk/strict-event-emitter-types: A type-only library for strongly typing any event emitter

NodejsのTypeScriptでGlobalな変数定義

ゲームプログラミングにて、様々なステータスを引数で渡すことなくアクセスするためのFluxライクなライブラリを書いた。その際にグローバル変数を定義する必要があり、すこしハマったので調べたことをメモ。

グローバルな型定義を行うにはおなじみの.d.tsファイルを使用する。どこに設置しても良いが、今回は/types/index.d.tsに設置する。また、必ず@types/nodeを入れる必要がある。

index.d.tsファイルの内容は以下のような形。

import { State, Dispatcher } from '../src/xxx/State';

declare global {
    namespace NodeJS {
        interface Global {
            state: State;
            dispatcher: Dispatcher
        }
    }
}

declare global宣言でモジュールの型定義を拡張することができる。もちろんNodejsのglobalオブジェクトはNodeJS namespace内に定義されているため、それを拡張することになる。@types/nodeを必要とする理由はこのことから。

また、index.d.tsをtsconfig.jsonで明示的に読み込む必要がある。

{
  "compilerOptions": {
    ...
    "typeRoots": ["types"], // 型定義ファイルを設置したディレクトリ
    "types": ["node"],
    ...
  }
}

これでglobalオブジェクトを使用する準備ができた。あとはコードを書けばコンパイルできる。

global.dispatcher = new Dispatcher();
global.dispatcher.dispatch('your_event');

以上です。

Powered by WordPress & Theme by Anders Norén