TDD programming software

ふつうの開発と TDD ワークショップ

TDD programming software

執行役員 VP of Engineering 兼技術部長の @hsbt です。9月に発売する LOST JUDGEMENT に備えて龍が如くシリーズの過去作品を一通りプレイし終えたので、次はモンハンストーリーズ2か何をプレイしようかなあと迷っている日々です。

GMO ペパボ(以下、ペパボ)では 2021 年の技術方針として「ふつうの開発をできるようになる」というスローガンを掲げています。

「ふつうの〜」という私が以前に所属していた永和システムマネジメントでよく使われていた形容詞です。すごいエンジニアがすごいテクノロジーを使ってすごいプロダクトを作って世界を変える、そういうやり方を夢見るのではなく、開発者一人一人が毎日の「ふつうの開発」のやり方のレベルを少しずつ高めていくことですごいプロダクトを作っていこう、という意味がこのスローガンにはこめられています。

ふつうの開発をできるようになる

では、ふつうの開発というのはどういうことでしょうか? 私が考える「ふつうの開発」は例えばこういう風景です。

  • GitHub のような課題管理/レビュープロセスを支援する仕組みを中心に開発プロセスを構築している
  • git を用いてプロダクトを手元に clone した後に開発者の熱が冷めないうちにコードを書き始めることができる
  • プロダクトには自動テストを実行する仕組みが備わっており、誰でもテストを追加し、実行することができる
  • pull-request は教育的レビュー、ピアレビューなど複数の観点でレビューが行われる
  • feature/topic branch は常に CI によってテストが実行され、開発者へフィードバックが迅速に行われる
  • リリース用のブランチにマージ後には開発者のデプロイへの熱量にかかわらず安全にプロダクトへコードがデプロイされる

上記以外にも、MVP をどのように作るのか、そもそも要件やニーズをどうソフトウェアへ落とし込むのか、関係者の期待に最も適したリリース日に合わせて開発をするなど、サービス開発には考慮すべきことはたくさんありますが、まずは開発者として、最も専門性を発揮できる領域について誰もができるようになるために様々な施策を進めています。今回ご紹介する TDD ワークショップの開催もその施策の一つです。

TDD ワークショップの開催

「ふつうの開発」は開発者のスキル(能力)とそれを効果的かつ効率的に発揮できる技術基盤、そしてエンジニアの文化が重要です。これまでは後者について、GitHub Enterprise の導入やオンプレミス環境での GitHub Actions の整備、k8s の導入によるデプロイ基盤などを重点的に進めてきましたが、開発者個々の能力や文化については独自企画による研修などが中心でした。

ペパボでは CI の整備により、自動テストによるテストコードを書く、という文化は当たり前になっていましたが、テストによってソフトウェア設計を行う、駆動するという活動は広くは行われていません。ペパボのエンジニアの評価の軸である「先を見通す力」を開発職が専門性を発揮していくには、より良いソフトウェアの設計を行えるということは必須と考えます。今年は改めて「ふつう」のレベルを上げていくために、Test Driven Development(TDD)の考え方をベースとしたソフトウェア設計の能力をペパボの開発者全員で上げていこうと考え、TDD の第一人者である @t_wada さんを招いてワークショップを開催することを決めました。

TDD ワークショップは 1 日 6h を使って Zoom を用いて開催しました。ここから先は実際に受講者としてワークショップに参加した @hayapi にバトンタッチしたいと思います。

研修内容

鹿児島エンジニアリングチームの@hayapiです!最近は自宅で毎日「Fit Boxing 2」で運動するために環境を整えています。

今回、TDDワークショップに1日参加した内容と学んだことをまとめていきたいと思います。

研修スケジュール

  • 10:00 〜 12:00 講演 + ライブコーディングによるデモ + 質疑応答
  • 12:00 〜 13:00 昼休み
  • 13:00 〜 16:30 ワークショップ + 1on1コードレビュー
  • 16:30 〜 17:00 全体レビュー + 質疑応答 + クロージング

ライブコーディングによるデモ

TDD Boot Camp 2020 Online #1で行われた基調講演/ライブコーディングの動画を事前に予習し、研修では動画が終わった時点のコードからスタートして、動画には収まりきらなかった応用的な内容とそれをベースにした議論から研修が始まりました。

動画で用いた FizzBuzz 問題とToDoリストの例は以下の内容です。

要件:
1から100までの数をプリントするプログラムを書け。
ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzzとプリントすること」
Fizz Buzz 数列とその変換規則を扱う FizzBuzz クラス
  convertメソッドは引数に与えられた整数を文字列に変換する
    3の倍数のときは数の代わりにFizzに変換する
      同値類の中の最小の3の倍数3を渡すと文字列Fizzを返す
      上の境界値のひとつ内側の値であり同値類の中の最大の3の倍数99を渡すと文字列Fizzを返す
    5の倍数のときは数の代わりにBuzzに変換する
      同値類の中の最小の5の倍数5を渡すと文字列Buzzを返す
      上の境界値であり同値類の中の最大の5の倍数100を渡すと文字列Buzzを返す
    3と5両方の倍数のときは数の代わりにFizzBuzzに変換する
      同値類の中の最小の3と5の公倍数15を渡すと文字列FizzBuzzを返す
      下の境界値のひとつ外側の値0は3と5両方の倍数でもあるので0を渡すと文字列FizzBuzzを返す
    その他の数のときは数をそのまま文字列に変換する
      下の境界値1を渡すと文字列1を返す
      下の境界値のひとつ内側の値2を渡すと文字列2を返す
      上の境界値のひとつ外側の値101を渡すと文字列101を返す

模範解答のサンプルコードはこちらにあります。

FizzBuzz問題のテストコードが書かれてから3年後、仕様を知らない新人エンジニアがテストコードをリファクタリングするにはどうすれば良いでしょうか?テストコードが書かれていてもテストケースが不十分だと仕様が読み取れずに結局実装を確認しないといけない、書いた本人に聞かないと仕様がわからないと言った問題が出てきます。テストの実行結果から仕様を読み取れることができれば、書いた本人がどういう意図でテストケースを書いたかわかるので、コードのメンテナンス性が向上します。講義では先述したような実装とテストコードの関係について学びました。

ワークショップ + 1on1コードレビュー

午後からは新しい演習問題を用いて、テスト駆動開発を前提とした開発に各自取り組みながら、@t_wada さんにコードレビューをして頂きました。

演習問題はこちら

まず初めにライブコーディングで学んだ方法を元に、ToDoリストに落とし込みながら仕様を理解することにしました。ToDoリストを整理しながら「どういったモデルが必要か」「どの実装から書いていくべきか」「仕様確認漏れがないか」など考えていきました。以下が、私が作成したToDoリストです。

## Todo
- [ ] 価格の確認
    - [ ] ワードプロセッサ「MS Word」の価格は 18,800円
    - [ ] ワードプロセッサ「一太郎」の価格は 20,000円
    - [ ] スプレッドシート「MS Excel」の価格は 27,800円
    - [ ] スプレッドシート「三四郎」の価格は 5,000円
- [ ] 収益認識の確認
    - [ ] ワードプロセッサ
        - [ ](ワードプロセッサ)2月1日に「MS Word」が1つ売れる契約が成立したとき、2月1日 に 18,800円収益認識される
        - [ ](ワードプロセッサ)2月1日に「一太郎」が1つ売れる契約が成立したとき、2月1日 に 20,000円収益認識される 
    - [ ] スプレッドシート
        - [ ](スプレッドシート)2月1日に「MS Excel」が1つ売れる契約が成立したとき、2月1日 に 18,534円
            - [ ] 3月3日 に 9,266円収益認識される
        - [ ](スプレッドシート)2月1日に「三四郎」が1つ売れる契約が成立したとき、2月1日 に 3,334円
            - [ ] 3月3日 に 1,666円収益認識される
    
- [ ] ソフトウェア製品(product) の登録
    - 名前の登録
    - 価格の登録
    
- [ ] 契約(contract)の登録
    - 契約日の登録
    - 売上の登録

### memo 
- ソフトウェア製品(product)
    - 名前(name)
    - 価格(price)
    - 種類(category)
- 契約(contract)
    - 契約日(signed_on)
    - 売上(revenue)
- 種類(category)
    - 名前(name)

ToDoリストを整理しながらテストを書いていき、最終的にテストコードの実行結果とToDoリストが一致することを目指してコーディングを進めていきました。 仕様を詰めていくと登場人物が増えたり登録・更新処理が増えたりと考慮すべき問題点が多く出てくるのですが、今回の研修の目的にあるようにテストを書いて動かしリファクタリングをするサイクルを多く回すことを意識しました。

% rspec spec/models/product_spec.rb

Product
  価格の確認
    ワードプロセッサ「MS Word」の価格は 18,800円
    ワードプロセッサ「一太郎」の価格は 20,000円
  収益認識の確認
    ワードプロセッサは契約日に直ちに売上全額を収益認識する
      2月1日に「MS Word」が1つ売れる契約が成立したとき、2月1日 に 18,800円収益認識される
      2月1日に「一太郎」が1つ売れる契約が成立したとき、2月1日 に 20,000円収益認識される
    スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する
      2月1日に「MS Excel」が1つ売れる契約が成立したとき、2月1日 に 18,534円、3月3日 に 9,266円収益認識される (FAILED - 1)
      2月1日に「三四郎」が1つ売れる契約が成立したとき、2月1日 に 3,334円、3月3日 に 1,666円収益認識される (FAILED - 2)

Failures:

  1) Product 収益認識の確認 スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する 2月1日に「MS Excel」が1つ売れる契約が成立したとき、2月1日 に 18,534円、3月3日 に 9,266円収益認識される
    Failure/Error: expect(contract.buy(product).revenue.to_i).to eq 18534

      expected: 18534
           got: 18533

      (compared using ==)
    # ./spec/models/product_spec.rb:40:in `block (4 levels) in <top (required)>'

  2) Product 収益認識の確認 スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する 2月1日に「三四郎」が1つ売れる契約が成立したとき、2月1日 に 3,334円、3月3日 に 1,666円収益認識される
    Failure/Error: expect(contract.buy(product).revenue.to_i).to eq 3334

      expected: 3334
           got: 3333

      (compared using ==)
    # ./spec/models/product_spec.rb:47:in `block (4 levels) in <top (required)>'

Finished in 0.05208 seconds (files took 2.85 seconds to load)
6 examples, 2 failures

Failed examples:

rspec ./spec/models/product_spec.rb:36 # Product 収益認識の確認 スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する 2月1日に「MS Excel」が1つ売れる契約が成立したとき、2月1日 に 18,534円、3月3日 に 9,266円収益認識される
rspec ./spec/models/product_spec.rb:43 # Product 収益認識の確認 スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する 2月1日に「三四郎」が1つ売れる契約が成立したとき、2月1日 に 3,334円、3月3日 に 1,666円収益認識される

コーディングを進めていくと、「2/3 で割り切れない売上が出たときに扱いをどうするか」という問題に直面しました。再度、問題文を見直すと以下のように書かれてあります。

「スプレッドシートは契約日に売上の2/3、30日後に1/3を収益認識する」

「なお、収益認識の総和は売上とかならず 完全一致 しなければならない。」

「例:2月1日に「MS Excel」が1つ売れる契約が成立したとき、2月1日 に 18,534円、3月3日 に 9,266円収益認識される」

ここが演習問題の落とし穴でした。@t_wada さんに仕様を確認したところ、「2/3 で割り切れない売上が出たときは契約日の方に端数分を1円切り上げて収益認識し、30日後に売上と収益認識を完全一致させる」とのことでした。実際の研修では演習が始まってすぐに端数の扱いを他の参加者が確認していましたが、ToDoリストに落とし込みながらテストを実行していたので気付くことができました。

感想

テストを書いて動作確認、リファクタリングのサイクルを回すというテスト駆動開発の一連の流れを体験することができました。テストファーストで実装を進めることで、満たすべき仕様を自分で整理できると同時に、第三者がテストコードを見れば仕様を把握できるメンテナンス性の高いコードに近づけることができるので積極的に取り入れていきたいです。

また演習問題の金額計算ロジックのように、事前に仕様の確認漏れが無いかチェックすることも大切であるが、テストケースがきちんと網羅されていて不具合に気付けるような状態を作り上げることも同様に意識するべきだと思いました。

t_wada さんによる開催後のコメント

@hsbt です。TDD ワークショップに興味がある方向けに、今回のワークショップ開催後に @t_wada さんからもコメントをもらったのでご紹介します。

工夫ポイント

反転学習方式

事前にいただいた情報で、今回参加される皆さんは日々テストを書いており、現場で活躍されている方々であることがわかっていました。このため、反転学習の要素を研修に組み込みました。具体的には、テスト駆動開発の基礎的な内容は予習用の動画をあらかじめ視聴してきてもらい、研修本編ではその動画を見てきた前提で、応用的な内容や踏み込んだ質疑応答から講義を始めることができました。

演習問題の難易度設定

参加される皆様のレベルが高めなので、普段よりも演習問題の難易度を上げました。演習時間に収まるくらいの小ささでありながら、金額計算に関するドメインロジックや現在時刻の扱いなど、テスト設計に注意を要する要素を入れ、複数のクラスの相互作用が必要な設計をテストコードを書きながら導いていくことを意図した作問を行いました。

所感

参加された方の中には演習問題に仕込んだ落とし穴に早いタイミングで気づいた方もおりました。演習問題としては高めのレベル設定をしましたが、結果的にはちょうど良いレベル感だったのではないかと考えています。自薦他薦を募った最終コードレビューにも勢いよく手が上がり、皆様テンション高く前のめりに参加されているという手応えを得ました。また、研修用の Slack channel を用意していただいたこともあり、当日までや当日のコミュニケーションが非常にスムーズになるなど、運営面でのサポートもたいへん助かりました。

終わりに

以上、ペパボで開催した TDD ワークショップについて、主催、講師、参加者のそれぞれの視点から開催レポートをお届けしました。

私も TDD によってソフトウェアの設計を行う、ということは本で読んだ内容やエンジニアとしてプロダクトコードをバリバリ書いていた頃にはやってみた、というレベルで経験がありましたが、今回のように改めてワークショップに参加することで「良いソフトウェアを作るための方法」としてのTDDの理解を深めるいいきっかけでした。今後もエンジニアとしての立場でも良いソフトウェアを作れるように研鑽を重ねたいと思います。