こまぶろ

技術のこととか仕事のこととか。

TypeScriptのユニットテストをmocha + power-assert で書く

故あってAngularを勉強しはじめた。

JSフレームワークとしては、Vue.jsを以前勉強していたので、「Angularではこうなのか」とか「これはVueにはなかったな」とか比較しながらドキュメントを読んで簡単な実装を進めている。

TypeScriptとJSフレームワーク

Angularの勉強をはじめてみて、当然出会うのがTypeScriptだ。

www.typescriptlang.org

Angularでは、TypeScriptが「開発の主要言語」とされており、公式のチュートリアルもTypeScript前提で進められる。三大フレームワークの中でも大規模開発との評判を聴くAngularにおいては、静的型付け言語であり、TS公式が"JavaScript that scales(スケールするJavaScript)"と自ら称するところのTypeScriptを採用するのが自然ということだろう。

TypeScriptは、Angularアプリケーション開発の主要言語です。

Vue.jsでも、TypeScriptを利用することは可能だ。ただし、あくまでもTypeScriptをサポートするという状態であって、標準的な開発言語はJavaScriptである。僕がVue.jsを勉強していたときも、JavaScriptで書いていた。

jp.vuejs.org

TypeScriptで『テスト駆動開発』の写経をする

TypeScriptに慣れ親しむために、『テスト駆動開発』の第1部の写経をしてみることにした。TypeScriptはJavaと構文が近しい言語だから、Javaのサンプルコードを見ながら写経をするのはハードルが高くないし、それでいて細かい部分でのJavaとTypeScriptとの違いを把握することができると考えたからだ。

https://github.com/ky-yk-d/ts-practicegithub.com

mocha + power-assert の環境を整える

『テスト駆動開発』の写経をするためには、ユニットテストの環境を整えなければならない。Angular CLIでは、プロジェクトやコンポーネントを作成したときに自動でテスト用のコードも用意されるようになっていて、そこで採用されているテスティングフレームワークはJasmineだ。

Jasmineでそのままやっても良かったのかもしれないが、それはAngularを使っていけば勝手に使うようになるだろうという考えもあって、以前記事にしたmocha + power-assert でのユニットテスト環境をTypeScriptでも整えてみることにした。

ky-yk-d.hatenablog.com

ちなみに、一度試してみて面白かったのは、下記の記事で紹介されているやり方だ。この記事は、とても丁寧に書かれていて勉強になった。カバレッジを自動で計測するというのはやったことがなかったので、これからまた取り組んでみたいと思う。

http://blog.catalyst-system.jp/useful-001/blog.catalyst-system.jp

espower-typescript を導入する

power-assertのReadMeをみてみると、

supports TypeScript.

とある。リンク先のページは、espower-typescript。ドキュメントの記載と実際の使い方を見た限りでは、TypeScriptに対応するだけでなく、上記の記事で利用していた intelli-espower-loader と同様の役割を果たしているようだ。intelli-espower-loader についてはこの記事に少し突っ込んだ記載があった。

espower-typescriptの導入方法は、ReadMeに書かれている。プロジェクトのルートで下記のコマンドを実行する。

npm install -D espower-typescript power-assert mocha typescript @types/node @types/mocha

ここで、-Dというオプションが登場している。「これはどういう意味なんだろう」と調べたところ、以前の記事で使っていた--save-devの省略表記であるとのこと。-dというオプションも別に存在しているが、全く意味が違う。実にややこしい。

https://docs.npmjs.com/cli/installdocs.npmjs.com

型定義ファイルについて

上のコマンドでは、以下の6つのパッケージをインストールしている。

  • espower-typescript
  • power-assert
  • mocha
  • typescript
  • @types/node
  • @types/mocha

馴染みのない@のついた下2つは、型定義ファイルを管理するものらしい。型定義ファイルというのは、Type ScriptのファイルからJavaScriptのライブラリを用いる際に必要になるもの。『速習TypeScript』から説明を引用しよう。

TypeScriptで本格的なアプリを開発するようになると、 JavaScript製のライブラリを活用したい、という状況は、ごく自然に発生します。もっとも、 JavaScriptライブラリには、 TypeScriptが要求する型情報が存在しないため、そのままでは正しくコンパイルできない場合があります。
そこで TypeScriptと JavaScriptとの橋渡しをするのが、型定義ファイルの役割です。型定義ファイルとは、名前の通り、型情報だけを定義したファイルのこと。型定義ファイルによって、 JavaScriptライブラリに型情報を与え、 TypeScriptが正しく認識(コンパイル)できるようにしてやるわけです。

TypeScriptとJavaScriptはスーパーセットとサブセットの関係にあるということで、ライブラリなども普通に使えると思っていたが、そういうわけでもないらしい。これをどのように扱うかについては、これまでさまざま苦労があったようだ。

qiita.com

テストを実行する

ツールを全て理解するのは困難だが、使うのは簡単だ。上のインストールさえ済んでしまえば、あとはテストとコードを書いて実行するだけだ。テスト側のコードは以下のようになった(『テスト駆動開発』第3章までの段階)。

import assert = require('assert');  // 公式ガイドに"CAUTION: don't use import 'assert' from 'assert'"とある
import { Dollar } from '../src/dollar';

describe('Moneyクラスのテスト', () => {

  it('$5 * 2 = $10', () => {
    const five: Dollar = new Dollar(5);
    let product: Dollar = five.times(2);
    assert(10 === product.amount);
  })

  it('Dollarの副作用がない', () => {
    const five: Dollar = new Dollar(5);
    let product: Dollar = five.times(2);
    assert(10 === product.amount);
    product = five.times(3);
    assert(15 === product.amount);
  })

  it('equals()メソッドが機能する', () => {
    assert(new Dollar(5).equals(new Dollar(5)));
    assert(!new Dollar(5).equals(new Dollar(6))); // 三角測量
  })

})

import ... = require('...') について

引っかかる場所があるとすれば、1行目のimport文だろうか。ReadMeに記載されているように、import ... from ...としてしまうと正しく動かない。

このimport ... = require( ... )という記法はTypeScript特有のものらしい。公式ドキュメントに"export = and import = require()" という項目があり、そこには以下のように書かれている。

When exporting a module using export =, TypeScript-specific import module = require("module") must be used to import the module.

モジュール側でexport =を使う場合はimport ... = require( ... )を使え、ということらしい。エクスポートする方法についても少し調べてみたが、どうも一筋縄では行かなさそうなので今回は調査を中断。CommonJSあたりの古い書き方、ということなのだろうか。

memememomo.hatenablog.com

実行コマンドをpackage.jsonに追加する

上の時点で、既にテストは動くようになっている。しかし、いちいちコマンドを叩くのは手間なので、例によってpackage.jsonのscriptにコマンドを書いておく。今回は、下記のように記載した。

  "scripts": {
    "mocha": "mocha --watch-extensions ts -w --require espower-typescript/guess test/**/*.ts"
  },

--watch-extensionsは、-wオプションをつけた際に監視対象にするファイルの拡張子を指定するものだ。デフォルトではjsファイルを監視対象としているので、jsファイルが存在しないプロジェクトの場合は-wオプションをつけても即座に終了してしまう。--watch-extensions tsとtsファイルを指定することによって、tsファイルが監視対象となり、-wオプションが期待通りに働くようになる。

また、--require espower-typescript/guess test/**/*.tsの部分は、

  • espower-typescriptのguessモジュールを利用すること(--require
  • テストファイルとして test配下のtsファイル全てを利用すること(test/**/*.ts

を指示している。JavaScriptのテストコードを実行するときは、末尾のファイル名指定は必要なかったが、それはmochaがデフォルトでtest配下のjsファイルを利用するようになっているからだった。今回はテストファイルもtsファイルであるから、明示的に指定しなければならない。**は再帰的に検索することを意味しており、--recursiveオプションと同じ効果を持っている。

テスト実行結果

あとはテストを実行するだけだ。npm run mochaでテストが実行される。実行結果は下記のようになる。

f:id:ky_yk_d:20181104022844p:plain

できた!

感想

JavaScriptからTypeScriptに変わっただけで、使うツールが同じなら環境構築も簡単だと思っていた。ところが、いざ始めてみると、JSのときは暗黙のうちにデフォルト設定の恩恵を受けていたことに気づいたり、TypeScriptという言語について勉強するきっかけになってよかった。

また、今回はコマンドのオプションが多かったが、調べながら進められた。プログラミングの勉強を始めたときに最初に習ったC言語で、当初は「おまじない」だと言われていた#include <stdio.h>の意味を少し進んでから理解したときの感覚に近いものがあり、懐かしかった。

思えば、気づかないうちに、全てのコード・全てのコマンドの意味を理解しながら進めようという姿勢を失いかけていたのかもしれない。もちろん、現実的には、全てを理解することはできず、調べてもすぐには分からないことに出会うことは避けられない。ただ、それを繰り返すなかで、学習性無気力になり、自ら調べることを放棄してしまうとしたら、エンジニアとしての死は間近だろう。

ここのところは技術記事にこだわらずにブログを書いていて、非常に自分としては手応えを感じているのだが、やはり技術的な内容のブログを書くことの効用は大きいな、と久し振りに実感した執筆だった。技術記事から逃げる形で「雑記」を書いているような形に陥ることがないように、下記のアドバイスを頭に置いておこう。