Open9

書籍「テスト駆動開発」の内容でTypeScriptとJestを使ってTDD(テスト駆動開発)をしてみる

きっかけ

この学習を始めようと思ったきっかけは以下の記事から

最初の頃って、自分でも「どう動いて欲しいのか」という所があいまいなまま書いていることが有って、動いてみて初めて「あぁ、これだ」と思うことが有ったのだけど、今なら早いうちからテストを書くことでそこに近づけると思う。「どう動いて欲しいか」という意図が明示的に書けるようになった段階で、コーディングを学んだと言えると思う。

https://blog.magnolia.tech/entry/2021/01/08/010257

書籍ではJavaを用いているので、TypeScript & Jestで学習が進められるか、そして、TDDによって振る舞いの明言化をすることでコーディングレベルが向上するかを記録してみます。

参考になりそうなリンク

https://zenn.dev/takahashim/scraps/61bd271722a129

https://www.youtube.com/watch?v=Q-FJ3XmFlT8

https://zenn.dev/yuma_ito_bd/articles/1612b1f0c92cf4

https://zenn.dev/higa/articles/34439dc279c55dd2ab95

https://zenn.dev/search?q=%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA

環境構築

今回は、以下の構成で進めたい。

種類 内容 ツール名
Unit 使い回す関数などのテスト Jest
Static 型や Lint などの静的なテスト TypeScript, ESLint

レポジトリ

レポジトリを作った。

https://github.com/ryosuketter/tdd-with-typescript-and-jest

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

ウォッチモードもできます。

以下からスタートすることにする。

https://qiita.com/jonakp/items/da5988b783cb28c03192#typescriptのインストール

コミット

https://github.com/ryosuketter/tdd-with-typescript-and-jest/commit/66ac1d971894dfd98247dc0651581b4e8f713bb8

コレもやるべきかな

https://photo-tea.com/p/17/javascript-test-how-to-5/

Jestのインストール

以下の3つをインストールします。

$ yarn add -D jest ts-jest @types/jest

package.jsonにtestの定義を追加

{
...
  "scripts": {
    "test": "jest"
  },
...
}

☝️これで、👇でjest実行できる

$ yarn test

https://nju33.com/notes/jest/articles/TypeScript と使う#TypeScript_と使う

引数2つ(数値)もらって合算して返す関数とその単体テスト(jest)を作ります。

src/sum.ts
export const sum = (a: number, b: number):number => a + b;
test/sum.test.ts
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の環境で動作してくれます。ブラウザ側では動作しません。

があります。

https://qiita.com/minato-naka/items/39ecc285d1e37226a283

もう一つは、何もしないと、JestはTypeScriptをテストできません。

これの解決策はJestの設定を定義することです。

設定を定義する場所は2つの選択肢があります。

  • 独立したファイルに定義する
  • package.json に定義する

https://qiita.com/hogesuke_1/items/8da7b63ff1d420b4253f

どちらでも問題はありません。

自分は👇を採用。

package.json(package.json に定義する)
{
・・・
  "license": "MIT",
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node"
  },
・・・
}
jest.config.js(独立したファイルに定義する)
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
};

preset に ts-jest をセットすれば、TypeScript をテストできるようになります。

ただ TypeScript が使えるようになる直接的な立役者は transform であり、preset は内部で transform の設定をしているだけです。

https://blog.ojisan.io/ts-jest
$ 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.

コミット

https://github.com/ryosuketter/tdd-with-typescript-and-jest/commit/4d786264c1ef32c0e8e5695f53db472f734f71b0

これでテストできるようになりました。

Prettierのみを使ってTypeScript, Jest を VS Code保存時に自動フォーマット

基本的に以下の記事を参照

https://zuma-lab.com/posts/eslint-prettier-settings

TypeScript, Jest で書かれたテストコードを VS Code でデバッグする

基本的に以下の記事を参照

https://blog.chick-p.work/debug-jest-vscode/

上記2つのコミット

https://github.com/ryosuketter/tdd-with-typescript-and-jest/commit/2d6c8ebcbe4feab641ff1fe3b37867e7c3203be2

それ以外に、以下も編集

User/settings.json
{
  ・・・
  "[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)
}

https://github.com/ryosuketter/tdd-with-typescript-and-jest/commit/379c525cbeeea322bc261fadb68b67956cfa4011

等価性のテスト(3章)

3章「三角測量」のとおりDollarインスタンスの値は不変であることをテストしないといけないらしい。

なぜなら、DDD(ドメイン駆動設計)のValue Objectパターンは「不変オブジェクト」であるということが言われている。そして、1を渡してDollarインスタンスを初期化したのなら、他の1を渡したDollarインスタンスと同等であることも主張されていて、それをテストで保証しないといけないらしい。

https://little-hands.hatenablog.com/entry/2018/12/09/entity-value-object

なので、等価性があることをテストしました。

https://github.com/ryosuketter/tdd-with-typescript-and-jest/commit/4b707ed1dcf1426124c349a3f36be4507d291f5e

ただ、三角測量がなんなのかはいまだにわかっていない。

意図を語るテスト(4章)

4章では等価性が定義できました。
Moneyクラスのアクセス修飾子は定義してありました。
また、timesメソッドの戻り値はMoneyオブジェクトであることはテストからもわかるので今回のコードの変更はありません。

原則をあえて破るとき(5章)

Meneyクラスのコードをコピーして、鋳型なFrancクラスにしただけなので、今回もコードの変更はありません。

ここではYenだが。

原則をあえて破るとき(6章)

  • dollarクラスとYenクラスはすでにMoneyクラスから継承されるように実装済み
  • equalsメソッドの一般化はすでに実装済み

なので、コードの変更はありません。

疑念をテストに翻訳する(7章)

  • YenオジェクトとDollarオブジェクトが等価でない検証はすでに実装済み
作成者以外のコメントは許可されていません