[[📚テスト駆動開発]]のメモ。
読書メモとあるがメインは写経なのでサブ的な存在。
> [!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