この記事は Angular Advent Calendar 2020 の 5 日目の記事です。
はじめに
昨今では TDD: Test Driven Development が注目されています。
TDD はテストを作成しながら開発を行えるので、テスト・カバレッジが増えてリファクタリングに強くなります。
変更への恐れが減り、常にコードを良い状態に保つためのよい循環を生むことができます。
設計手法としての TDD
先月、2 週間の認定スクラムデベロッパー研修を受けました。
そこで学んだ TDD は設計手法としての TDD です。
Test First でコードを書いていくことで、デザイン(コードの構造設計)の決定をできるだけ先送りすることができます。
ここでいう先送りとは、ネガティブな意味ではなく、全体が見えていない段階で手戻りが多くなってしまう構造設計を決める必然性を生ませないことを意味します。
むしろコーディングがある程度進んだ段階でデザインを熟慮して、既に実装した挙動には影響を与えることなくより適したデザインを反映させたリファクタリングができることになります。
実験的施行
この記事では、Angular でのフロントエンド開発においての TDD を試みています。
今回の試行と考察が議論の種になればと思っています。
TDD の流れ
Test First で開始して、次に実装を行います。
これが 1 サイクルになります。
ステップ | 内容 | 詳細 |
---|---|---|
Step 1 | テストを追加して RED にする | まず、ひとつの振る舞いに対してテストやアサートを書いて、テスト実行結果が RED になる(失敗する)ことを確認する |
Step 2 | GREEN になる最小限の実装をする | テストやアサートが GREEN になる(成功する)ように、最小限の実装を行う |
Step 3 | GREEN を維持してリファクタリングをする | テストやアサートが GREEN になること維持したまま、リファクタリングをする |
ポイントは RED から GREEN するときに、それを満たす最小限の実装を行えばよい点です。
RED になるように追加したひとつの振る舞いに対してのみ実現できるコードに集中することができます。
例えば、null 値や上限値への対応は、次のサイクルで null 値で RED になるテストやアサートを書いたときに初めて考慮していくことになります。
必要な要件をひとつひとつ振る舞いとしてテストやアサートを追加していくことで、実装済みのコードを壊すことなく、徐々に目的のコードへ進化させていくことができます。
題材
今回の題材としては、よくあるカウンターを使ることにします。
最終的には、現在のカウントが表示されていて、+ をクリックすると加算される画面を作ります。
少し特殊ですが、最大値を 5 として、それ以上にはならない仕様にします。
ストーリー
ストーリーとして分けた実装内容です。
これらを順に実装していきます。
- ユーザーとして、現在の数字が確認できる
- ユーザーとして、 + ボタンで加算ができる
- カウンターとして、最大値は 5 で、それ以上にはならない
実装
TDD のステップにのっとって実装をしていきます。
準備
まず、ng new tdd-front-counter
で Angular アプリケーションを生成します。
生成直後は app.component.html
に placeholder のコードがあるので、このコードは削除します。
そのコードに関するテストコードも ``app.component.spec.ts` から削除します。
yarn start
と yarn test
を実行しておきます。
Story 1: ユーザーとして、現在の数字が確認できる
最初に、カウンターの値が確認できるようにします。
Step 1. テストを追加して RED にする
app.component.spec.ts
に下記テストを追加します。
it('should see the current count.', () => {
expect(app.count).toEqual(0);
});
当然テストは RED になります。
すばらしい!
RED になったことを祝福します。
Step 2. GREEN になる最小限の実装をする
このテストを満たすための、最小の実装をします。
app.component.ts
に count
を初期値 0
で用意するだけです。
export class AppComponent {
count = 0;
}
Step 3. GREEN を維持してリファクタリングをする
ここで、テンプレートで count
を表示するコードを書きます。
<div class="app">
<p>{{ count }}</p>
</div>
テストは GREEN を維持しています。
アプリ画面には、count
の初期値である 0
が表示されました。
ここで Story 1 は完成です。
リファクタリングをします。
count
を表示する部分は、コンポーネント化して切り出しました。
テストは GREEN を維持しています。
<div class="app">
<app-count-panel [count]="count"></app-count-panel>
</div>
@Component({
selector: 'app-count-panel',
templateUrl: './count-panel.component.html',
styleUrls: ['./count-panel.component.sass'],
})
export class CountPanelComponent {
@Input()
count = 0;
}
<p>{{ count }}</p>
Story 2: ユーザーとして、 + ボタンで加算ができる
次に、カウンターを増やす振る舞いを実装します。
Step 1. テストを追加して RED にする
順序にのっとって、まずはテストを書いて RED にします。
it('should increase the count.', () => {
app.addCount();
expect(app.count).toEqual(1);
})
当然 addCount()
が未定義なので、テストは失敗して RED になります。
すばらしい!
RED になったことを祝福します。
Step 2. GREEN になる最小限の実装をする
このテストを満たすための、最小の実装をします。
最小限の実装は、なんとこうなります。
addCount(): number {
this.count = 1;
}
すべてのテストが GREEN なので、問題ありません!
コーディングは一番簡単なことを考えればいいのです。
Step 3. GREEN を維持してリファクタリングをする
テンプレートに + ボタンを追加して、クリックで addCount()
が呼ばれるようにします。
<div class="app">
<app-count-panel [count]="count"></app-count-panel>
<button (click)="addCount()">+</button>
</div>
Step 1. テストを追加して RED にする
このままだと、当然 + ボタンを複数回クリックしたときに対応できていません。
もちろん、まずテストに振る舞いを追加します。
it('should increase the count.', () => {
app.addCount();
expect(app.count).toEqual(1);
app.addCount();
expect(app.count).toEqual(2);
})
また、テストが RED になりました!
Step 2. GREEN になる最小限の実装をする
ここで、みなさんがイメージしていたコードになります。
addCount(): number {
this.count = this.count + 1;
}
Step 3. GREEN を維持してリファクタリングをする
Story 2 は完成です。
リファクタリングをします。
ボタン部分は、コンポーネント化して切り出しました。
テストは GREEN を維持しています。
<div class="app">
<app-count-panel [count]="count"></app-count-panel>
<app-button (click)="addCount()">+</app-button>
</div>
@Component({
selector: 'app-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.sass']
})
export class ButtonComponent { }
<button>
<ng-content></ng-content>
</button>
Step 3. カウンターとして、最大値は 5 で、それ以上にはならない
ボタンをクリックしていくと、 5 まででストップするようにします。
Step 1. テストを追加して RED にする
さて、テストを書きましょう。
it('should not increase the count more than 5.', () => {
[...Array(5).keys()].map(() => app.addCount());
expect(app.count).toEqual(5);
app.addCount();
expect(app.count).toEqual(5);
});
Step 2. GREEN になる最小限の実装をする
定数 MAX
を 5 で定義して、MAX
以下のときだけ加算するように実装します。
const MAX = 5;
...
addCount(): void {
if (this.count < MAX) {
this.count = this.count + 1;
}
}
Step 3. GREEN を維持してリファクタリングをする
ここでは特にないのでスキップします。
Story 1〜3 までの実装が終わって、アプリケーションが完成しました。
まとめ
今回は、シンプルなアプリケーションでの TDD を試してみました。
説明した実装の流れと、試行錯誤した考察をまとめます。
実装の流れ
冒頭で説明した TDD のステップに、今回のフロントエンドでの実装を加えると、こうなります。
まとめると、今回の試験的な実装では、このような流れになりました。
ステップ | 内容 | 詳細 |
---|---|---|
Step 1 | テストを追加して RED にする |
app.component.spec.ts に振る舞いのテストをひとつ足す |
Step 2 | GREEN になる最小限の実装をする |
app.component.ts に最小限の実装をする |
Step 3 | GREEN を維持してリファクタリングをする |
app.component.html に画面表示の実装をする。コンポーネント化をする。今回は省略しましたが、スタイルの実装もここになります。 |
考察
- コンポーネント単位に引っ張られやすく、振る舞いに対してテストを書く意識を持つことに慣れが必要
- コンポーネント作成のタイミングでコンポーネント単位で
*.spec.ts
が生成されるので、コンポーネントに対してのテストに引っ張られやすい - View Model をコンポーネント単位の service に集約して service に対してテストを書くことも考えたが、こちらも結局コンポーネント単位の発想に引っ張られてしまって振る舞いのテストになりにくい
- 今回はシンプルだったので、
app.component.spec.ts
に書いていけた - TDD としては画面単位で振る舞いテストを記述していくことが適するように思える
- モジュールやコンポーネント単位では、それぞれのテストを書いていくのが良さそう
- 今回はリファクタリングのステップで View を実装するようにしたが、View に対して Test First にすることは難しい
- シンプルな画面を題材にしたが、条件分岐
ngIf
やループ処理ngFor
を入れると別の知見が得られそう
おわりに
実装したアプリケーションは shioyang/tdd-front-counter にあります。
ストーリーとステップごとに commit してあります。
明日、Angular Advent Calendar 2020 の 6 日目は @fusho-takahashi さんです!