書籍「テスト駆動開発」の内容でTypeScriptとJestを使ってTDD(テスト駆動開発)をしてみる
きっかけ
この学習を始めようと思ったきっかけは以下の記事から
最初の頃って、自分でも「どう動いて欲しいのか」という所があいまいなまま書いていることが有って、動いてみて初めて「あぁ、これだ」と思うことが有ったのだけど、今なら早いうちからテストを書くことでそこに近づけると思う。「どう動いて欲しいか」という意図が明示的に書けるようになった段階で、コーディングを学んだと言えると思う。
書籍ではJavaを用いているので、TypeScript & Jestで学習が進められるか、そして、TDDによって振る舞いの明言化をすることでコーディングレベルが向上するかを記録してみます。
参考になりそうなリンク
環境構築
今回は、以下の構成で進めたい。
種類 | 内容 | ツール名 |
---|---|---|
Unit | 使い回す関数などのテスト | Jest |
Static | 型や Lint などの静的なテスト | TypeScript, ESLint |
レポジトリ
レポジトリを作った。
SETUP手順
プロジェクトの作成
プロジェクトディレクトリを作成して、package.jsonを作りましょう。
$ mkdir tdd-with-typescript-and-jest
$ cd tdd-with-typescript-and-jest
$ yarn init
yarn init後に色々聞かれるがわかりそうなところだけ答えた。
TypeScriptのインストール
$ yarn add -D typescript @types/node
typescriptは開発でしか使わないので、オプション-D
をつける。
tsconfig.json
tsc
コマンド使ってコンパイルもできますが、tsconfig.jsonを作って、一気に全対象ファイルをコンパイルできるようにします。
$ tsc --init
message TS6071: Successfully created a tsconfig.json file.
無事、ファイルが生成された後、
$ tsc
と、打てばコンパイルされるようになりました。
$ tsc -w
ウォッチモードもできます。
以下からスタートすることにする。
コミット
コレもやるべきかな
Jestのインストール
以下の3つをインストールします。
$ yarn add -D jest ts-jest @types/jest
package.jsonにtestの定義を追加
{
...
"scripts": {
"test": "jest"
},
...
}
☝️これで、👇でjest実行できる
$ yarn test
引数2つ(数値)もらって合算して返す関数とその単体テスト(jest)を作ります。
export const sum = (a: number, b: number):number => a + b;
import { sum } from '../src/sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
テスト開始
$ yarn test
エラー出た。
yarn run v1.22.0
warning ../../package.json: No license field
$ jest
FAIL test/sum.test.ts
● Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
/Users/ryosuke/Project/tdd-with-typescript-and-jest/test/sum.test.ts:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import { sum } from '../src/sum';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.876 s, estimated 3 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
内容をそのまま翻訳すると
Jestがパースできないファイルをインポートしようとしていることを意味しています
(例:プレーンなJavaScriptではない)
とのことらしい。
まず、import(ESM)は、ES6で決められたモジュール読み込みの仕様なので、プレーンなjs(ts)だと読み込めません。これがエラーの1要因です。
補足
モジュールの読み込みの主な種類は
- ESM (ECMAScript Modules) ← ES6で決められたモジュール読み込みの仕様。ブラウザによっては動作します。
- CJS (CommonJS Modules) ← Nodejsの環境で動作してくれます。ブラウザ側では動作しません。
があります。
もう一つは、何もしないと、JestはTypeScriptをテストできません。
これの解決策はJestの設定を定義することです。
設定を定義する場所は2つの選択肢があります。
- 独立したファイルに定義する
- package.json に定義する
どちらでも問題はありません。
自分は👇を採用。
{
・・・
"license": "MIT",
"jest": {
"preset": "ts-jest",
"testEnvironment": "node"
},
・・・
}
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
preset に ts-jest をセットすれば、TypeScript をテストできるようになります。
ただ TypeScript が使えるようになる直接的な立役者は transform であり、preset は内部で transform の設定をしているだけです。
$ yarn test
$ jest
PASS test/sum.test.ts
✓ adds 1 + 2 to equal 3 (4 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.138 s
Ran all test suites.
✨ Done in 2.74s.
コミット
これでテストできるようになりました。
Prettierのみを使ってTypeScript, Jest を VS Code保存時に自動フォーマット
基本的に以下の記事を参照
TypeScript, Jest で書かれたテストコードを VS Code でデバッグする
基本的に以下の記事を参照
上記2つのコミット
それ以外に、以下も編集
{
・・・
"[typescript]": {
"editor.formatOnSave": true
},
・・・
}
感想
P4から始めている。
テストを書くときに想像するのは、用途に合った完璧なインターフェイスだ。
これどう言う意味なのだろう?
やっと多国通貨の実装に入れる
お金という概念から、多国の通貨という実態を実装していくので、まずは Money クラスを用意する。
引数は必要か?通貨なので、金額(amount: number)と、どこの通貨(currency: string)という情報が必要でしょう。
constractor(インスタンスを生成する際に自動的に呼び出されるメソッド)の定義はJSと同様ですが、TSでは、型宣言が必要になります。
また、アクセス修飾子はprotectedにします。なぜなら、今後、別のクラスに同様の機能を実装するのでインターフェイスをimplementsさせたいからです。互換性を保証するためにimplementsキーワードを使うことができます。
今回は、USドルを生成するクラスに紐づくメンバ(静的メンバ)も定義しておきます。そうすることで、インスタンスを生成しなくても静的メソッドを実行できるからです。
class Money {
protected amount: number
protected cur: string
constructor(amount: number, currency: string) {
this.amount = amount
this.cur = currency
}
times(multiplier: number): Expression {
return new Money(this.amount * multiplier, this.cur)
}
static dollar(amount: number): Money {
return new Money(amount, 'USD')
}
}
こんな感じで実装できるでしょう。
Moneyクラスのメソッドに掛け算を追加しています。
times(multiplier: number): Expression {
return new Money(this.amount * multiplier, this.cur)
}
掛け算のテストを実装(2章)
Moneyクラスの掛け算メソッドの単体テストをテストを実装。
import { Money } from '../src/money'
describe('Money module', () => {
test('times()', () => {
const five = Money.dollar(5)
expect(five.times(2)).toEqual(Money.dollar(10))
})
})
等価性のテスト(3章)
3章「三角測量」のとおりDollarインスタンスの値は不変であることをテストしないといけないらしい。
なぜなら、DDD(ドメイン駆動設計)のValue Objectパターンは「不変オブジェクト」であるということが言われている。そして、1を渡してDollarインスタンスを初期化したのなら、他の1を渡したDollarインスタンスと同等であることも主張されていて、それをテストで保証しないといけないらしい。
なので、等価性があることをテストしました。
ただ、三角測量がなんなのかはいまだにわかっていない。
意図を語るテスト(4章)
4章では等価性が定義できました。
Money
クラスのアクセス修飾子は定義してありました。
また、times
メソッドの戻り値はMoney
オブジェクトであることはテストからもわかるので今回のコードの変更はありません。
原則をあえて破るとき(5章)
Meney
クラスのコードをコピーして、鋳型なFranc
クラスにしただけなので、今回もコードの変更はありません。
ここではYen
だが。
原則をあえて破るとき(6章)
-
dollar
クラスとYen
クラスはすでにMoney
クラスから継承されるように実装済み -
equals
メソッドの一般化はすでに実装済み
なので、コードの変更はありません。
疑念をテストに翻訳する(7章)
-
Yen
オジェクトとDollar
オブジェクトが等価でない検証はすでに実装済み