[[📚テスト駆動開発]]のメモ。 読書メモとあるがメインは写経なのでサブ的な存在。 > [!caution] > 本ノートは読書のメモであって**要約ではない**。過不足もあれば間違った内容もある。あくまで個人の感想やメモに過ぎないことを前提に読んでほしい。 ## オレオレルール 本書は[[Java]]や[[Python]]で書かれているが、言語は[[TypeScript]]で同じ意味の内容を書いていく。狙いは2つ。 - [[Java]]や[[Python]]と比べて、[[TypeScript]]が普段最も使う言語だから - 丸写しできない環境をつくることで理解度を深めるため なお、[[IDE]]は[[Neovim]]を使用する。これは単に[[Neovim]]力を高めたいから。具体的には操作スピードと環境の向上である。 ### リポジトリ [[Deno]]でプロジェクトを作成し、リポジトリは[[GitHub]]に公開する。 <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">GitHub - tadashi-aikawa/typescript-tdd</p> </div> <div class="link-card-description"> Contribute to tadashi-aikawa/typescript-tdd development by creating an account on GitHub. </div> </div> <img src="https://opengraph.githubassets.com/18dce40c7ede3e58e58edd9e8661e83972e03b9293191642b4d5d4299d444ae0/tadashi-aikawa/typescript-tdd" class="link-card-image" /> </div> <a href="https://github.com/tadashi-aikawa/typescript-tdd"></a> </div> https://github.com/tadashi-aikawa/typescript-tdd/tree/a32d2bac22ec03a481f159044239e97db6b729ff > [!attention] > 途中で[[Bun]]に変更している。 ## Are you ready? ここからが本編。 # まえがき ### テストの価値 - 開発が予測可能になる - チームの信頼感が増す - 書いていて気持ちいい ### 作業の順序 | 用語 | 意味 | | ---------------- | -------------------------------- | | レッド | 動作しないしコンパイルも通らない | | グリーン | テストが成功する? | | リファクタリング | 重複の除去 | ### 勇気と不安 - テスト熱中症 ### 本書を読み終えたあとの姿 - シンプルに始める - 自動テストを書く - リファクタリングで設計判断を1つずつ行う # 第I部 多国通貨 **対応順序** 1. テストを1つ書く 2. 全テストを実行させ、1のテスト失敗を確認する 3. 小さな変更を行う 4. 全テストを実行させ、1のテスト成功を確認する 5. リファクタリングで重複を削除する ## 第1章 仮実装 細かいステップを踏み続けられる(can)であることが大事。 > [!positive] 普段の開発でもログ出力やダミー実装で休憩ポイントを設けて、少しずつ開発していってるからこれは分かる。[[TDD]]とは別に、通常の開発も (遠回りにならない程度の) 小刻みなステップは大事だと思う https://github.com/tadashi-aikawa/typescript-tdd/tree/40459251658d1acbccc41b6179b8d6a5d524420e ## 第2章 明白な実装 | 用語 | 意味 | | ---------- | -------------------------------------------- | | 仮実装 | 定数ベタ書きからはじめて変数に置き換えていく | | 明白な実装 | 頭の中の実装をすぐコードに落とす | > [!thinking] 自信があるときは明白な実装モード、自信がなくなったら仮実装モードにシフトというのは分かりやすい戦略の1つ。自分は仮実装モードからスタートすることが多いけど、無意識のうちに2つのモード切替を実はしているのかもしれない...🤔 https://github.com/tadashi-aikawa/typescript-tdd/tree/31e10a05c5da7ccff43d4fc3964fdff279a660be ## 第3章 三角測量 - [[Value Object パターン]] - [[別名参照問題]] https://github.com/tadashi-aikawa/typescript-tdd/tree/6e5b45e5e0479705dd4378aeefc547a2931e5e81 ## 第4章 意図を語るテスト > [!positive] 『テストは意図を語る』という表現好き。複雑な処理を隠蔽するための関数化も大事だけど、テストコードのインターフェースは読めば伝わる方が遥かに大事だと思っている。 https://github.com/tadashi-aikawa/typescript-tdd/tree/43ec65f155173e2c31b3efac42107f6ecdaeac66 ## 第5章 原則をあえて破るとき > [!positive] 自分はよく『正義のコピペ』という名前で呼んでる https://github.com/tadashi-aikawa/typescript-tdd/tree/fc5cac29059b8b9745fe19083a5ed803c9c6a460 ## 第6章 テスト不足に気づいたら https://github.com/tadashi-aikawa/typescript-tdd/tree/24c9fa78e5fbd3e18c14869abbb404114741a1b3 ## 第7章 疑念をテストに翻訳する https://github.com/tadashi-aikawa/typescript-tdd/tree/5a95444f02f3a5424db89810d4e51f5589936a26 ## 第8章 実装を隠す https://github.com/tadashi-aikawa/typescript-tdd/tree/5358cd78e9ad0d3dcb55cddb7eb15abf316a24f0 ## 第9章 歩幅の調整 https://github.com/tadashi-aikawa/typescript-tdd/tree/93354efed73108be92e94b123623fc1f69d401b9 ### [[Deno]]から[[Bun]]に移行 ここでリポジトリに大きな変更を加えてみる。 - [[Windows]]のホストではなく、[[WSL2]]の[[Ubuntu]]を使う - [[Deno]]から[[Bun]]に変える 理由は[[Bun]]の方が好みであり、[[Bun]]は[[WSL2]]でしか動かないから。 https://github.com/tadashi-aikawa/typescript-tdd/tree/b84cc64a2abc5559a40b42a88f0af010d3a2e2dc ## 第10章 テストに聞いてみる https://github.com/tadashi-aikawa/typescript-tdd/tree/cad2bd20588e21d9e3b0abfab8177e437b63e331 ## 第11章 不要になったら消す https://github.com/tadashi-aikawa/typescript-tdd/tree/2ba6d5cddb536f1c21d8a3ba4502c36ba07f4b06 ## 第12章 設計とメタファー https://github.com/tadashi-aikawa/typescript-tdd/tree/fd0a49d9c7c6f155dfccc6fe0dcaf34cfab5112f ## 第13章 実装を導くテスト - `augend`: 被加算数 - `addend`: 加算 > [!negative] ここら辺から設計が先読みしすぎるような気がする。今までのように納得感があっての展開というより、筆者の経験に伴った勘が主体的になっている雰囲気を覚えた。 https://github.com/tadashi-aikawa/typescript-tdd/tree/ba17079274282ad3222bb619de992b0f83a5811b ## 第14章 学習用テストと回帰テスト - [[TypeScript]]だと等価性テストには`toBe`を使う。これが`===`相当 - `toEqual`は[[オブジェクト (JavaScript)|オブジェクト]]や[[配列型 (TypeScript)|配列]]の等価性 - おそらく、各要素について `toBe` しているはず... ```ts test("Arrayの等価性テスト", () => { expect(["abc"]).toBe(["abc"]); // NG }); ``` [[TypeScript]]には[[Java]]のように`hashCode`や`equals`を実装することによって、[[ハッシュテーブル]]等価判定を行えるようにする機構がない。だが、[[Bun]]には`deepEquals`が実装されており、これを使うことで同等の要件を満たせる。 ```ts test("bunの等価性テスト", () => { expect(Bun.deepEquals(["abc"], ["abc"])).toBeTrue(); expect(Bun.deepEquals({ a: "A", b: "B" }, { a: "A", b: "B" })).toBeTrue(); }); ``` `Map`の等価判定が実装されていないので `this.rates.get(new Pair(from, to))`は`rates`に登録されたアイテムを取得できない。 ```ts export class Bank { private rates: Map<Pair, number> = new Map(); reduce(source: Expression, to: string): Money { return source.reduce(this, to); } addRate(from: string, to: string, rate: number) { this.rates.set(new Pair(from, to), rate); } rate(from: string, to: string): number { return from === to ? 1 : this.rates.get(new Pair(from, to))!; } } ``` 最終的には`Pair.ts`を削除して以下のような実装にした。 ```ts export class Bank { private rates: Map<string, number> = new Map(); private hash(from: string, to: string): string { return `${from},${to}`; } reduce(source: Expression, to: string): Money { return source.reduce(this, to); } addRate(from: string, to: string, rate: number) { this.rates.set(this.hash(from, to), rate); } rate(from: string, to: string): number { return from === to ? 1 : this.rates.get(this.hash(from, to))!; } } ``` https://github.com/tadashi-aikawa/typescript-tdd/tree/ea81520362ef68567bc951eead22fba7886c8ca0 ## 15章 テスト任せとコンパイラ任せ > [!negative] Expressionという用語が(自分の)直感にあわなくて、あまり頭にスッと入ってこない... > [!positive] コンパイルエラー任せで修正していくとゴールにたどり着く感じは[[DDD]]で何度も体験したから分かりみが深い - `Money`クラスの`plus`[[メソッド (TypeScript)|メソッド]]は既にpublicにしていたのでそこはスルー https://github.com/tadashi-aikawa/typescript-tdd/tree/81e4b9ff7fc707ed61adcc5ba357a0afcd771781 ## 16章 将来の読み手を考えたテスト > TDDをやっていると、テストコードとプロダクトコードの行数は、同じくらいになる。つまりこれまでの2倍コードを書けるようになるか、半分のコード量で同じだけの機能を書けるようになるかしないと、TDDのメリットを得られないということだ。 > [!thinking] やはりスピードは正義だなあと改めて。 このテストが通らない。 ```ts test("同じ通貨でPlusするとMoneyが返却される", () => { const sum = factory.dollar(1).plus(factory.dollar(1)); expect(sum instanceof Money).toBeTrue(); }); ``` `Expression`は`Money`の基底クラス。`sum`は`Expression`のインスタンス。[[TypeScript]]の`instanceof`はインスタンスの派生クラスに対して`true`とならない。 ```ts import { expect, test } from "bun:test"; class Base {} class Derived extends Base {} test.each([ [new Base() instanceof Base], // OK [new Derived() instanceof Base], // OK: 子クラスインスタンスが親となら [new Base() instanceof Derived], // NG: これはダメ ])("", (condition: boolean) => { expect(condition).toBeTrue(); }); ``` `Money.plus`の実装はたしかに`Sum`インスタンスを返却しているので`sum instanceof Sum`なら通る。 ```ts export class Money implements Expression { plus(addend: Expression): Expression { return new Sum(this, addend); } ``` ただ、`Sum`の基底クラスも`Expression`であるため、`Money`と同様に`true`にはならない。 https://github.com/tadashi-aikawa/typescript-tdd/tree/3ccf330d7ff193ff953466c2c5d2c4f352412ea4 ## 17章 他国通貨全体のふりかえり - [[TDD]]を厳格に行う場合、[[ステートメントカバレッジ]]は100%になる - [[ステートメントカバレッジ]]は品質を測るための十分な指標にはならない (とっかかりにはなる) - [[欠陥挿入]] > [!positive] [[カバレッジ]]を上げるためにテストの量を増やすより、プログラムロジックをシンプルにした方がよい。超同意。 # 第Ⅱ部 xUnit [[#第Ⅱ部 xUnit]]の内容は `part2` ディレクトリの中に格納し、以下のコマンドで変更したら即座に実行されるようにしておく。 ```console cd part2 bun --watch test ``` > [!caution] `xunit.ts`では`bun --watch test`でテストできない > 当初は以下のようにするつもりだった。 > ```console > cd part2 > bun --hot xunit.ts > ``` > ただ、[[ソースコード内にテストコードを書く (Bun)]]するのが厳しそうだったので断念した。 [[#第I部 多国通貨]]でやった内容は`part1`ディレクトリに移動する。 ## 18章 xUnitへ向かう小さな一歩 単純に以下のように書くと... ```ts export class WasRun { constructor(public name: string, public wasRun?: number) {} run() { const method = (this as any)[this.name] as CallableFunction; method(); } testMethod() { this.wasRun = 1; } } ``` 以下のようなエラーになる。 ```error 3 | run() { 4 | const method = (this as any)[this.name] as CallableFunction; 5 | method(); 6 | } 7 | testMethod() { 8 | this.wasRun = 1; ^ TypeError: undefined is not an object (evaluating 'this.wasRun = 1') ``` これは `method()` の呼び出し内で `this` が [[undefined]] になるため。`.bind(this)`で`this`を束縛するやり方が1つ。 ```diff run() { - const method = (this as any)[this.name] as CallableFunction; + const method = (this as any)[this.name].bind(this) as CallableFunction; method(); } ``` それか、例外処理をしないなら以下のようにワンライナーで書いてしまう。 ```ts run() { ((this as any)[this.name] as CallableFunction)(); } ``` `xunit.ts`ではテストが実行できないので、`xunit.ts`とは別に`xunit.spec.ts`を作り、そちらでテストすることにする。 ```console bun --watch test ``` > [!thinking] テストしながら進めることが大事でもあるけど、テストでなくても小さく分解して確認しながら進めること、そして分解できることの価値がひしひしと感じてくる。つまるところはTODOリストが作成でき、少しずつ進められるか。そこにテストは必ずしもなくてもいいと思う。あった方が圧倒的に楽だけど。 https://github.com/tadashi-aikawa/typescript-tdd/tree/f716b635e1a8bf27605841c5fa9dc239498a3d66