はじめに
こんにちは、ピクトリンク事業部開発部サーバサイド開発課、兼ホグワーツ留年中のkitajimaです。
先日、Certified Scrum Developer®(CSD®)のトレーニングを受講しました。
今回はその中で特に実践の時間が設けられており長時間取り組んだ、TDD(Test-Driven Development | テスト駆動開発)について紹介させていただきます。
CSDトレーニングとは
本トレーニングは、スクラムチームの開発者として、正しくかつ効率的に恊働できる人材育成を目的としてScrum Alliance®により作られた、体系的ソフトウェア開発者の教育・認定プログラムです。高い技術力を持つエンジニアへ成長させ、参加者が良いスクラムの実践者となれるようにサポートします。理想的なスクラムチーム の1週間のスプリントを体験する過程で、小さなアプリケーションを構築しながら、適切な知識や技術、チームとして効率的に働く習慣を得ます。トレーニング中にアジャイルコーチからコーチングも受けられるので、実践での悩みも相談できます。
Scrum Alliance®認定 スクラムトレーニング|オッドイーより引用
受講の経緯
私たちのチームは以前から開発フレームワークとしてスクラムを採用していましたが、昨年度まではコンポーネントチーム、つまりバックエンド、フロントエンド、モバイルアプリといった担当領域ごとにチームが分かれている状況でした。
上記のようなコンポーネントチームでは、以下のような課題がありました。
- チーム間のコミュニケーションコスト増大
- チーム同士で整合する内容が多い
- 1チームで完結できない作業が増える
- 1つのストーリーを完成させるために複数チームが絡むため、スプリントゴールがブレたり、待ち時間が発生したりする
以上の課題を改善すべく、今年度から担当領域を混合した機能横断チームに体制を変更しました。チームで作業が完結させられ、スクラムに集中できることが狙いです。
また、一人ひとりに得意領域がありつつも、その領域をお互いに超えて作業ができる状態を理想ともしています。
この新体制でスクラムに集中し、より素早く価値を提供し続けられるよう、チームから私を含む2名が本トレーニングを受講することとなりました。
CSDトレーニングの流れ
3日間(計18時間)のオンライン研修でした。
- 1日目
- スクラム、アジャイルのintroduction
- TDD
- 2日目
- good test
- TDD 実践編
- スクラムウォークスルー(スプリントの流れ、ポイントの説明)
- 自己管理
- 3日目
- リファクタリング
- TDD 実践編続き
- ペアプログラミング
- CI(継続的インテグレーション)
このように、TDDでコーディングを実践する時間が多く取られていました。コーディングはブラウザ上のエディタ(cyber-dojo)を使用し、初対面の受講者の方とペアで行いました。緊張の瞬間です...!
TDD
TDDの手法を、FizzBuzzやポーカーを題材にして学びました。 TDDの考え方が普段意識するものではなく、難しかったのでFizzBuzzを例に紹介します。
テストケースを1件書く
まずテストケースを1つ書きます。FizzBuzzの仕様の最小のものである、「1を与えると1を返す」というケースです。
@Test @DisplayName("1を与えると1を返す") void case1() { String actual = FizzBuzz.execute(1); assertEquals("1", actual); }
テストを満たす最小の実装をする
上記テストケースをpassするための最小の実装を実現します。
public static String execute(int input) { return "1"; }
はい、これで大丈夫です。今の時点での全テストケース(1件)がpassしました。
え!?
安心してください。これはテストケースを満たす最小の実装であり、仮実装と呼ばれるものです。
テストケース追加
それでは、ここからもう1ケースを追加してみましょう。FizzBuzzは、「2を与えると2を返す」振る舞いがあります。
@Test @DisplayName("1を与えると1を返す") void case1() { String actual = FizzBuzz.execute(1); assertEquals("1", actual); } @Test @DisplayName("2を与えると2を返す") void case2() { String actual = FizzBuzz.execute(2); assertEquals("2", actual); }
この状態で実行すると当然テストはfailです(期待する失敗と言います)。
実装を修正
2件のテストがpassするように実装を修正します。
public static String execute(int input) { if (input == 1) return "1"; return "2"; }
一般化
そしてこの2件の仕様を「一般化」します。 「1を与えると1を返す」、「2を与えると2を返す」、...つまり「nを与えるとnを返す」という実装にすればいいのか、と辿り着くわけです。
どのように一般化すべきか、具体例を積み重ねる工程は「三角測量」と呼ばれます。
今回はFizzBuzzという単純な題材なのですぐに一般化できるでしょう。(それすら超えて、今の時点で皆様は「早くFizzBuzz、Fizz、Buzzのケースを実装してよ」とお思いのことでしょう)
題材が難しい場合はテストケース及び仮実装を積み重ねていき、見えてきたタイミングで一般化に踏み出しましょう。
それでは、今の実装をリファクタリング(=2件のテストケースがpassする事実を保ったまま、実装を修正)します。
public static String execute(int input) { return String.valueOf(input); }
ここでもう一度テストを実行しても、2件ともpassするはずです。
Fizzのテストケースを追加
続いて、FizzBuzzのキモとなるケースを追加してみます。「3を与えるとFizzを返す」です。
@Test @DisplayName("3を与えるとFizzを返す") void case3() { String actual = FizzBuzz.execute(3); assertEquals("Fizz", actual); }
Fizzを通すための実装修正
今の実装だとcase3はfailするので、これがpassするように実装します。
public static String execute(int input) { if (input == 3) return "Fizz"; return String.valueOf(input); }
case3が通りました。
Fizzの別パターンのテストケースを追加
続いて「6を与えるとFizzを返す」ケースも追加します。
@Test @DisplayName("3を与えるとFizzを返す") void case3() { String actual = FizzBuzz.execute(3); assertEquals("Fizz", actual); } @Test @DisplayName("6を与えるとFizzを返す") void case4() { String actual = FizzBuzz.execute(6); assertEquals("Fizz", actual); }
今の実装ではcase4がfailになります。
Fizzの2つの仕様を満たすための実装修正
実装を以下のように修正します。
public static String execute(int input) { if (input == 3 || input == 6) return "Fizz"; return String.valueOf(input); }
そしてcase4が通ります。
ここで、Fizzの場合についても実装を一般化できそうです。以下略...
このように、fail→impl→pass→refactor→...と、テストケース(=仕様)を満たす最小の実装を追い続けることで実装していく手法がTDDです。
cyber-dojoではテスト実施結果の履歴が視覚的に分かるようになっており、failとpassを繰り返して思い通りのTDDができているか確認しながら進められました。
※イメージ
🔴🟢🔴🟢🔴🔴🟢🔴🟢🔴🟢🔴🔴🟢🔴🔴🔴🟢🔴🟢...
FizzBuzzの例の続きを、ぜひTDDの考え方で最後まで実装してみてください!
TDDのポイントだと感じたところ
小さい歩幅で進む
仮実装の積み重ねとテストケースの追加を細かい歩幅で進めていきます。
仮に途中で上手くいかなかったとしても「この時点では通っていた」というチェックポイントがこまめに設定されているので安心です。
スプリントと同じように、「検査を小さいサイクルでやっている」と捉えるとスクラムの理念と合致しそうです。
また、歩幅に正解はなく、1メソッドごとでなくても構いません。人によっても感覚は異なります。ペアプロ、モブプロの場合はメンバーの感覚を尊重して進め方を決めたいです。
テストの捉え方
TDDを実践していると、テストは追加の工程ではなく、実装をしていたら自然についてくるものと言えます。 実装→テスト というフェーズが分かれているのではなく、実装を進めるにあたって当然テストも一緒にできているもの、と捉えられると嬉しいです。
先に実装ができてしまった...
ある程度実装が進むと、まだテストケースに挙げていないような仕様も勝手に満たしていたり、これまでのコードで一般化が簡単にできることがあります。 テストケースより実装が先行してしまうことは実際起きて普通であり、特に問題はありません。しかし、実装とテストの距離が離れないように、タイムリーにテストを書くことが求められます。
所感
どのくらいの歩幅でテストと実装を積み重ねていくか、は人によって好みの感覚が異なるのではないかと思いました。チーム開発、特にモブプロやペアプロをする際はメンバー同士で歩幅のすり合わせが必要だと感じています。
また、慣れるまではTDDの適用が容易そうな箇所、例えばドメインオブジェクトなど外部に依存しないコードで試してみようと思いました。
さいごに
CSD研修で学んだプラクティスのうち、今回はTDDについて紹介させていただきました。
機会があればチームでTDDを実践してみた経過もお伝えできればと思っております。
お読みいただきありがとうございました。