(この話は最初Twitterに書こうと思ったけど、長くなるのでブログに書くことにしました)
僕はRSpecやMinitestでテストを書くのは得意ですが、常にテストファースト(TDD)で開発するとは限りません。
今業務でやってるタスクはこんなふうに進めてます。
雑に動くものを作る
↓
見た目をきれいにする&機能を作り込む
↓
テストを書く
↓
リファクタリングする
この順番で開発する理由を以下に述べます。
雑に動くものを最初に作る理由
最初は見た目とか、異常系とか、細かい仕様とかを無視して、正常系が一通り動くものを作ります。
これはこれから作ろうとしているものの認識が合っているかどうかをPO(プロダクトオーナー)に確認するためです。
実際に動く画面を見せると「こんな感じでOK」とか「ここはこういうふうにしたい」というフィードバックをもらうことができます。
また、開発者としてもコードを書きながら「あれっ、こういう場合はどういう仕様にしたらいいんだろう?」という考慮漏れの仕様が見つかったりすることがあります。
Tips: あとで作り込む部分は忘れないように適宜TODOコメントを入れておくと良いです。
その次に見た目をきれいにしたり、機能を作り込んだりする理由
方向性を確認したら、見た目や機能の詳細を作り込んでいきます。
この時点でテストを一緒に書いていくこともありますが、「テストを書くのは時間がかかりそうだな」と思ったら、テストを全く書かずに進めることもありますたとえば、その機能に関連する既存のテストがなく、テストコードをゼロから書かないといけない場合などです。(一方、「これはテストを先に書いた方が、手と目で動作確認を繰り返すより速くなりそうだな」と判断することもあります。よって、書く、書かないはケースバイケースです)。
こうする理由は万一、何らかのトラブルでスケジュールが遅れそうになったときに「テストもないし、コードも汚いけど、最悪この状態でリリースすることは可能」という状態に持っていくためです(あくまで最悪のケースですよ!!)。
「テストをがんばって書いてたら当初のリリーススケジュールをオーバーしてしまった」よりも、「テストはまだ書けてないけど、とりあえずリリースはできた。順番が逆になったけど、リリース後にテストを書こう」の方がビジネス的には望ましいはずです。
補足:テストを書かずにリリース=不具合を残したままリリース、ではない
テストを書かずにリリースするときでも、最低限以下の2点はクリアしておく必要はあります。
- 追加した機能についてはしかるべき担当者が手と目で動作確認している(=手動テストはクリアしている)
- 既存の自動テストはすべてパスしている
つまり、「テストを書いていない」というのは「デグレしないことを担保する自動テストがないだけ」の状態です。
決して「手動テストすらやらずに、不具合の可能性を残したままリリースする」という意味ではありません。
もちろん、「テストを書かずにリリース」はあくまで最後の手段であって、やむを得ない理由がない限りやっちゃダメです🙅♂️
反響を見てると、あたかも僕が普段からテストを書かずに本番リリースやってるかのようなコメントが多いけど、そんなわけないからね!もし「やんごとなき理由で開発が遅延した&納期はズラせない」という状況になっても大丈夫なように、という話ですから!!計画通り進めばテスト付きでリリースされます✅ https://t.co/is0wU0J7DG
— Junichi Ito (伊藤淳一) (@jnchito) 2023年2月17日
最後にテストを書いてリファクタリングする理由
一通り実装が終わったらテストを書きます。テストファーストならぬ、「テストラスト」ですね。
一通り実装が終わって「最悪このままでもリリースできる状態」になっているので、精神的な余裕をもってテストを書くことができます。
ちなみに「リファクタリングをしてからテストを書く」のではなく、「テストを書いてからリファクタリングする」というのがポイントです。
リファクタリングは「リファクタリングの前後でプログラムの挙動が変わらないこと」が大前提です。
この「挙動が変わらないこと(リファクタリングが原因でプログラムが壊れたりしないこと)」を担保するためにはテストコードの存在が必須です。
もしテストより先にリファクタリングをやってしまうと、挙動が変わらないことを開発者自身の手と目で担保しなければなりません。
これは非効率ですし、うっかりミスでプログラムを壊してしまう恐れもあります。
参考:コードレビューも分割して良い
作成する機能が大きいときは上記のステップが終わるたびにプルリクエスト(PR)を作成して、コードレビューしてもらいましょう。
すなわち、
雑に動くものを作る
↓
PR作成&コードレビュー
↓
見た目をきれいにする&機能を作り込む
↓
PR作成&コードレビュー
↓
テストを書く
↓
PR作成&コードレビュー
↓
リファクタリングする
↓
PR作成&コードレビュー
とするわけです(テストとリファクタリングはまとめてコードレビューでも可)。
あ、もちろんコードレビューが終わってもmainブランチにはまだマージしませんよ。mainブランチにマージするのはリファクタリングが完了してからです。
特に「雑に動くものを作る」段階で一度コードレビューしてもらうことは重要です。
なぜなら、コードレビューを受けることで技術的な方向性の根本的な誤りを指摘してもらえるかもしれないからです。
また、最後にまとめてPRをどーん!!と作成すると、diffが大きくなりすぎてレビュアーが苦労する可能性もあります。
PRはなるべく小口化することを心がけてください。
プルリクエストをレビューするとき、僕は変更されたファイル数で大きさを見積もることが多い。感覚としてはこんな感じ👇
— Junichi Ito (伊藤淳一) (@jnchito) 2020年9月29日
- 10以下 = すぐ見ま〜す😄
- 10〜30 = まあふつう
- 30〜50 = げ、ちょっとしんどいな
- 50〜100 = 大きすぎる。もっと小さくして!
- 100以上 = 見たくない!出直してきて!! pic.twitter.com/wlCIS1jasD
テストファーストで作ることはないの?
いえ、もちろんテストファーストで作るときもあります。
僕は以下の条件がどちらもYESになったときにテストファーストで開発します。
- プログラムの仕様が明確である(作りながら「どうしようかな?」と考える部分がない)
- その仕様を担保するテストコードをすらすら書ける自信がある(=テストを書くコストが低い)
たとえば、「 "alice@example.com" を "a...e@example.com" のように、ユーザー名の最初と最後の1文字だけ残してマスキングするメソッドを作れ」という要件があったりした場合は、上の2つの条件を満たすので次のようなテストコードを先に書いてしまいます。
RSpec.describe User, type: :model do describe '#masked_email' do context 'ユーザー名が3文字以上' do example do user = User.new(email: 'ken@example.com') expect(user.masked_email).to eq 'k...n@example.com' user = User.new(email: 'alice@example.com') expect(user.masked_email).to eq 'a...e@example.com' end end context 'ユーザー名が2文字' do example do user = User.new(email: 'me@example.com') expect(user.masked_email).to eq 'm...e@example.com' end end context 'ユーザー名が1文字' do example do user = User.new(email: 'x@example.com') expect(user.masked_email).to eq 'x...x@example.com' end end end end
上のテストコードは何もドキュメントを見ずに、テストも一切実行せずに、エディタ上でひたすらコードを打ち込んだものです。
あとはmasked_email
メソッドを実装してテストを全部パスさせればいいだけですね。
こういうケースはテストファーストで開発するスタイルが有用です。
不具合修正するときもテストファースト
他にも不具合を修正するときもテストファーストで修正します。
具体的には以下のような流れです。
エラーを再現させるテストコードを書く(この時点ではテストの結果はRED🚨)
↓
エラーを修正する(場合によっては修正する前に、デバッガも併用しながらテストを実行してエラーの原因を調査する)
↓
テストがパスすることを確認する(テストの結果はGREEN✅)
テストを書いておけば不具合の再発も防止できるので一石二鳥です✌️
あわせて読みたい
そもそもテストコードってなんで必要なんだっけ?という基礎から学びたい方はこちらのスライドをどうぞ。
RSpecを使ってRailsのテストをすらすら書けるようになりたい!という人は、「Everyday Rails - RSpecによるRailsテスト入門」をどうぞ。
leanpub.com