TDD Boot Camp in 愛媛 に参加しました!

TDD Boot Camp in 愛媛 #1 - connpass

TDDBC( 2 / 15 sat ) から 2 週間経ちましたが、いまだ興奮冷めない感じで日々の開発に活かせること満載な勉強会でした!改めてここで振り返りをします。

www.instagram.com

前提

講演(ラブコーディング付き)

ここでは私的感動ポイントをつらつら書いていきます。

きちんとした説明は実際の TDD Boot Camp に行くか、動画(50 分でわかるテスト駆動開発)もあるみたいなので、そちらを見てください。

動作するきれいなコード - どうたどり着くか

動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+α - t-wadaのブログ

TDD は「動作するきれいなコード」を目指すための手段なのですが、この記事ではもう少し大きな概念の話で、「動作するきれいなコード」への道のりや、目的達成を阻む人類の性質にどう対応してきたか?今後どうしたらいいのか?など、とても面白かったです。

気になったことメモ

  • 三脚椅子のメタファーの話🤣
  • リファクタリングの付箋は剥がされない😂
  • 単一作業に集中できる黄金の回転
  • リファクタリングは弱いので、独立タスクにしない
  • Unit テストと E2E テストのフィードバックの違い

「使いやすい」コードのためのテストファースト

もともと TDD という言葉は知っていましたが、「テストを先に書く」が私の頭の中で一人歩きしており・・なぜ、テストを先に?後に書いても同じじゃない?とモヤモヤしてた疑問が一気に解消されるお話でした。

「使いやすい」コードを目指す

  • テストを先に書くことで、そのプログラムを使う側(読む側)の視点からテストが書ける
  • 先にプロダクトコードを作ってしまうと、使う側(読む側)の視点を見えなくなる
  • 作りやすいコードと、使いやすい(読みやすい)コードは違う。そして、使いやすい(読みやすい)コードを目指すべき

なるほど!🤩

「恐怖に支配された世界」から脱出するためのテスト

昔は「動いてるコードに手を付けるな」と言われてましたが、今は周りの環境(動作環境やライブラリなど)がどんどんアップデートされていくため、例え「塩漬け(手を付けない)」していても動かなくなる場合がある。

そのため、テストコードを書くことで

  • 安心して既存コードに手が付けられる
  • 常にこのコードは正しく動くのか?が判断できる

ようになり、安心を得ることができる。

以前、テストコードを書いていなかった時は、新しくコードを書いた部分のリリースでさえ恐怖を感じていました。そしてテストコードを書くようになった今でも、自分の書いたコードにバグがあったらどうしよう・・😨という恐怖がゼロになることは有りませんが、圧倒的に少なくなったなぁと思いますし、バグが出た時でも再発防止の修正が気軽になったなぁと思います。

テスト実装の優先順位

テストの優先度を決めるための軸は2つ。

  1. テストが書きやすいか?
  2. 重要度が高いか?

そして、最初は「1 番テストが書きやすい」Todo から倒す(テストを書く)と良い。
※ テストを書くこと自体に不慣れ・不安な場合の挫折予防です。

Todo はテストを書きやすい形にする

講演のお題は定番の「FizzBuzz」問題でした。

1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。

参考)どうしてプログラマに・・・プログラムが書けないのか?

最初の Todo の作り方

Todo は上記に書いた FizzBuzz の問題文を元に、以下の作業を行います。

  • 問題文をタスク(Todo)に分割する
  • Todo 全体で表現、語尾などを揃える

Todo がどんどんブラッシュアップされてく感じはライブコーディングがとても分かりやすかったです!😍すごい!
この段階で全体を眺め、表現方法を合わせたり、粒度を揃えたりできるの良い!

テストを書きやすくする(テスト容易性をあげる)

例えば、以下の Todo があった場合

  1. 数が 3 の倍数のときは「Fizz」とプリントする

「プリントする」というのはテストが書きにくいのに、重要度が低い(そのテストができても嬉しくない)ので、以下のようにテストを分割する。

  1. 数が 3 の倍数のときは「Fizz」に変換する
  2. プリントする

そうすると、 (1) はテストし易くなり、(2) はテストが書きにくく、重要度が低い Todo として優先度を下げることができる。

テストは日本語(母語)で書いたほうが良い

テストコードは「動く仕様書」の要素が今後さらに強くなってくるので、母語で書いた方が良い。

テストはゴール(検証)から作る

テストで書くもの

  1. 前準備
  2. 実行
  3. 検証(ゴール)

テストはゴール(検証)から書こう!
なぜなら、複雑なテストの場合、前準備が数十行になってしまうと

  • ゴール(検証)が分からなくなる
  • ゴール(検証)を複数用意したくなる(頑張って作った前準備を有効活用したい欲)

という罠にハマってしまう可能性大!

(テストアンチパターン)1 つのテストに多数の検証

1 つのテストに多数(検証が縦にいっぱい並ぶ)の検証を書いた場合、途中の検証が失敗するとそこでテストが止まるので、1 度のテストでどれが失敗するのか?判断できない。また、どの検証が失敗しているのか分かりづらいので、アンチパターンです。

これは「ただ乗り(The Free Ride / Piggyback)」と呼ばれるようです。
参考)TDD Anti-patterns catalogue at Stack Overflow を簡単に訳してみた - joker1007’s diary

Rspec だと 1 テスト 1 検証が一般的になっている気がしてて、私がテストを書き始めた頃、ネット上の記事を参考にしてたのですが、ほとんどの記事が 1 テスト 1 検証(1 expect to)だったような気がします。gem RuboCop RSpec を入れるとデフォルトで 1 テスト 1 検証しか書けなくなりますし。
参考)RuboCop::Cop::RSpec::MultipleExpectations

ただ、1 テスト 1 検証だからといって、以下のように事前データを配列 or Hash を使ってまとめて書かれると、失敗したときに何個目で落ちたの??になりやすく、辛い思いをすることが多いです。せめて it にループした時の具体値を埋め込むなど、別の人でも検証しやすいテストコードが作れるよう気を付けたいです。

[true, false, true].each do |boolean|
 it "状況に応じた結果が返されること" do
  expect(boolean).to eq true
 end
end

また、テストパターンによっては 2 つの検証が成功しないと意味がないものもあるので、それを 1 つのテストにまとめるべきか否かはメンバーと要相談かもです。

残酷な瞬間との早期出会い(抽象から具体)

「いざ書き始めようとすると指が動かない」という残酷な瞬間に早めに出会える。

例えば、以下の Todo

  • 数を文字列に変換する

だと、期待値は何にしたらいいのか?テストで何を検証したらいいのか?🤔になるので、以下のように具体値を使った Todo を追加する。

  • 数を文字列に変換する
    • 1 を渡すと文字列 1 に変換する

実際、プロダクトコードを書いてても、頭の中では書けそうなのに指が動かない・・みたいなことあります・・😂 自分の深掘りが甘ければ、遅かれ早かれ残酷な瞬間に出会うことになるので、テストを通して「そのタイミングを早める」という考え方が素敵です。

テストコードのテスト

「テストコード自体にバグがあったらどうするの?」という問いは確かに・・と思いました。結局、テストも人が書いてますからね・・。前職ではテストを目視でやっていたのですが、テストをする人がミスしても品質が担保できるよう、ダブルチェッカー体制にしてたこともありましたが・・じゃぁ 2 人とも間違えたら・・?とか、考え始めたらキリがないですね。。

テストの書き始めにテストコードをテストする

これは「テストコードのテスト」を最小限コストに抑えるためです。

テストコードのテスト方法

  • 最短で Green にする
    • テストの期待値とプロダクトコードの返り値に同じ具体値を入れて Green にする
      • テストで expect "1"、プロダクトコードで return "1" にする
  • テストコードのテストをする
    • 意図的に判別可能なバグを入れて Red にする(欠陥挿入)
      • テストで expect "1"、プロダクトコードで return "2" にする

例え Red であっても期待通りに動いていれば成功なのです!でもこの「テストコードのテスト」は、慣れてくると飛ばしがちです・・。

テストの実行順はランダムにする

ライブコーディングでは Java を使っており、 Java のテストツール(JUnit)はデフォルトで実行順序をランダムにしてくれるそうです。知らなかった!

ランダムにすることのメリット

  • 各テストの依存関係がなくなる(A のテストを実行しないと B は成功するか分からない、などがなくなる)
  • 依存関係をなくすことで、テスト完了スピードが遅くなった時に並列実行に移行できる

Rspec の場合

デフォルトでランダムになっているだろうと予測してたのですが、設定ファイル(spec_helper.rb)に明示する必要がありました。私の環境では、Rspec を設置した時に自動生成した設定ファイルの実行順序指定(config.order = :random)はコメントアウトされており、これまで定義順でテストしていたことが判明しました🙄

spec_helper.rb

# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
#     --seed 1234
config.order = :random

設定ファイルの実行順序を指定しない場合、rspec コマンドのデフォルトである defined(定義順)が反映されるようです。
参考)`--order` option - Command line - RSpec Core - RSpec - Relish

また、実行順序に依存したい場合、seed 番号(Kernel.srand config.seed)を使ったり、テストケースごとに順序指定したりできるみたいです。

不安があるなら歩幅を小さくする

テストコードの実装に対する不安を和らげるため、自信を付けるために細かいステップを踏む(歩幅を調整する)ことが可能です。細かいステップは「不安を埋めること」が目的なので、石橋は叩いても良いし、叩かなくても良いのです。そのため、以下の手順は任意です。

  • 仮実装(return "1" とかの固定値で返す実装のこと)
  • 三角測量(複数の道を試すこと、1 の場合、2 の場合など)

3 年後の誰か(自分を含む)のためにテストを書く

無駄なテストコードを消せるのは今の自分だけ

例えば、テストコードに不慣れで、歩幅を小さくしようと以下のように複数の値をテストした場合

  • 1 を渡すと文字列 1 に変換する
  • 2 を渡すと文字列 2 に変換する

今の自分にとっては安心に繋がる大切なステップでしたが、テスト全体としては「1」のテストが通れば「2」のテストを行う必要がないため、無駄なテストといえます。そして、これが無駄だと明確に判断できるのは今の自分だけです。

このテストをもし 3 年後の誰かが見た際、「この人はテストを書くことに不慣れだったらか、2 つの値を検証しているのだろう」なんて思わず、むしろ「あえて 2 つ書いてるのは意味があるのだろう。1 と 2 で挙動が変わるのかなぁ。まぁないよりあった方がいいし、プロダクトコード確認するの面倒だし、とりあえず置いておこう」と思うのがほとんどで、永遠に無駄なテストが残り続けます👹

無駄なテストを永続させないために、毎日 or 毎週など定期的に(記憶があるうちに)テストコードを整理しましょう!不慣れな自分に自信を与えてくれたテスト達を消せるのは、今の自分だけなのです!

また、3 年後の誰かは「未来の自分」の可能性も高く、過去のコードを見て「コレなんだろう・・?😕意味わからん」って思ったら自分だった😇パターンは悲しいですし(あるあるですが・・)、他人だったら迷惑かけても良いって訳でもないので、不要なテストをチェックする習慣は付けていきたいです。

足りないテストコードを追加できるのも今の自分だけ

上と同じ話ですが、もし FizzBuzz 問題のテストを書いている時、他の作業が忙しくなってしまった場合。「(1), (2) だけはちゃんとテストを書いた、(3) は目視で確認して動いてたっぽいし、残りは後でやろう〜」とか考えて放置したテストコードを 3 年後の誰かが見た時、どう思うでしょうか?

  1. [x] 数が 3 の倍数のときは「Fizz」に変換する
  2. [x] 数が 5 の倍数のときは「Buzz」に変換する
  3. [ ] 数が 3 と 5 の倍数のときは「FizzBuzz」に変換する

「(3) はテストが書き辛かったのかな?重要じゃないからテストしてないのかな?🤔まぁどちらにせよ、無理してテストを追加する必要はないか。変に追加して失敗しても面倒だし」になり、永遠にテストが足りない状態 & 機能追加・修正がやり辛い状態になります💀

仕様がテストコードだけで分かる状態が理想

せっかくテストが書かれていても、3 年後にプロダクトコードを確認しないと実装を理解できない状態は勿体ないです。

テストの粒度は誰がテストするのか?で変わる

「実装を知ってる人が書くテスト」と「実装を知らない人が書くテスト」は異なる(ベストプラクティスが違う)。実装を知らない人は品質保証を求める場合が多く、テストが手厚くなりがち。そして、品質保証を重視するテストにするか?はプロジェクトごとに選択できる。

意味のある数値でテストしてますか?

例えば、FizzBuzz 問題で

  • 数が 3 の倍数のときは「Fizz」に変換する

をテストする時、2 つぐらいの具体値でテストしておけば品質高まるかなぁと考え、具体値を 2 つ用意する。

  • 数が 3 の倍数のときは「Fizz」に変換する
    • 数が 3 のときは「Fizz」に変換する
    • 数が 6 のときは「Fizz」に変換する

そして、品質保証の方から「なぜ 3, 6 を選んだの?」と聞かれ、「なんとなくです・・」となりがち。とりあえず 2 つ選べばいいと思ってしまった、自分の浅さにに気づく。

意味のある数値の 1 つとして境界値という提案。境界値は要件に書いてあることが多い。FizzBuzz の場合、「1から100までの数をプリントするプログラムを書け。」なので、99 を選ぶと良さそう。

ペアプログラミング

私のペアは Rails で自社サービスを作成している K2iwai さんで、Ruby + Rspec で TDD ペアプロをしました。

(良かった)ペアで知識の基礎レベルが同じだった

お互い

だったので、例えば以下のような前提が当たり前になるので、そもそも論で情報格差がなく、とてもペアプロしやすかったです。

  • 一般的にファイル名・メソッド名はスネークケース、クラス名は頭大文字を使う
  • クラスの初期化処理は initialize メソッドを使う
  • getter, setter は attr_accessorを使う(メソッドの綴りは調べないと書けなかったけど)
  • 真偽値判定のメソッドは ? を使う
  • メソッドの最後の式を返り値にする場合は return を省略できる
  • Rpsec は describe, context, it を使う
  • テストファイルは接尾語に _spec を付ける

(気付き)お互いの拘りをシェアするのが楽しい

お互い仕事でプログラムを書いてる分、仕様書やコードへの拘りが少しずつあり、それをシェアし調整しながら実装するのはとても勉強になりました。また、自分が何に拘ってるのか?なぜ拘ってるのか?や、逆にそこまで拘ってない部分などが見えて面白かったです。

(私の拘り)Todo やテストを仕様書にしたい

私は実装より日本語の表現に拘っていることが判明しました。

最初は以下のように Todo を作ってたのですが

  • 区間クラスを初期化する
  • 区間クラスは下端点と上端点のプロパティを持つ

クラスとかプロパティって仕様書らしくない!と考え

  • 整数閉区間を作成する
  • 整数閉区間は下端点と上端点を持つ

に変更しました。

私の中で「仕様書らしい」というのは「プログラマー以外の人が読んでも理解できる」だと考えます。

(K2iwai さんの拘り)メソッド名は短い方が良い

例えば、お題の閉区間を表すのに ClosedSection を提案した時、「短くしたい!」と言われ Range を検討しました(結局、Ruby の組込みクラスだったため断念し、ClosedSection に落ち着きました)。私はメソッド名の長さをほとんど気にしておらず、むしろ分かりやすくなるなら、ある程度長くても良いと思ってる派です。kkd さんに「今は editor 補完が可能なので長さを気にする人は少なくなった」と教えて頂きました。

(K2iwai さんの拘り)引数のクラスが異なる場合メソッドを変えたい

今回のお題で「閉区間に含まれる」を表現する時

  1. 区間に数値が含まれるか?
  2. 区間に別の閉区間が含まれるか?

という、2 つのパターンが存在しました。個人的には 1 つのメソッドを使いメソッド内で実装を分ければ・・?と思ったのですが、K2iwai さんは別のメソッドが良いと言っていたので分けることにしました。
ただ、メソッドを分けた場合、以下のようになるのが嫌だったので、新しい単語を考えました。

  • include_number?
  • include_closed_section?

結果、(1) を between? 、(2) を include? にしたのですが、コードレビューで (1) の between? は意味が逆じゃない?と指摘され確かに・・となり、kkd さんから Ruby の組込みクラスである Range を参考に members? や cover? はどうか?と提案いただき、それいい!🤩となりました。改めて Ruby命名の素晴らしさを感じました。

(気付き)役割分担が自然とできた

最後の方は私が Todo の修正やテストコードを作り、 K2iwai さんが実装する、的な流れができてるように感じました。

そのため、私がテストコードを書いたら K2iwai さんにお願い!ってパスしたり、私が例外発生の書き方で迷ってたら「ちょっと貸して」とドライバーを奪われたり、多分、最初に t-wada さんが話してた「良いペアプロ」ができたんじゃないかなぁと思います。

また、程々に性格が似てた(ざっくりな感じとか)ので、小さいことで褒め合ったり、ちょっと実装してすぐ休憩したり(笑)で楽しかったです。

次回、K2iwai さんとペアプロする時は、あえてプロダクトコードを私が、テストコードを K2iwai さんが、というように書ける場所を制限するのも面白そうだなぁと思いました。

(気付き)t-wada さんの説明通りの手順を踏んでた

最初はテストコードだけでなく、ペアプロ自体にも不安(慣れない感)が合ったので、割と歩幅を小さくしながら(仮実装、三角測量を織り交ぜながら)実装しました。しかし、後半はテストコードの書き方、ペアプロの感じも分かってきたので、歩みを刻まず、いきなり「テスト → 実装」に自然となっていました。また、後からテストコードの一覧を見返した際に「無駄」を発見し整理する作業(消す・文言を調整するなど)もしたので、「わ!t-wada さんの言ってた通りになってる!」と思いました。

コードレビュー

前でレビューしてもらいました!

コードレビュー後の差分です。
コードレビュー後 · dobby618/tddbc-ehime-1st@c97541e · GitHub
※ 問題等も上げているので、もし TDDBC にこれから参加するよ!という方は、当日楽しむために見ない方がいいかもです。

TDD 後に行動が変わったこと

テスト一覧を確認するようになった

$ rspec -f d --dry-run --order defined

参考)RSpecで作ったexampleの一覧をテストの実行なしに出力する - Qiita

一覧を眺めながら

  • この一覧だけでやりたいことが伝わるか?
  • 仕様書らしい表現を使っているか?
  • 表現が揃っているか?
  • 階層が揃っているか?
  • 無駄なテストはないか?
  • 足りないテストはないか?

などを確認するようになりました。
これまでテストを書いたら終わり!だったのですが、一覧を確認するようになり俯瞰する視点が身に付いたように感じます。

三者に伝わる言葉を考えるようになった

例えば、会員登録で以下の 2 つ画面に別れるようなパターンで

  • ユーザ情報(名前など)の登録画面
  • 趣味の登録画面

「ユーザ情報」の登録済み判定を「氏名」の登録有無で判断していた場合

context '氏名を登録している場合' do
 it '趣味の登録画面に遷移する' do
  expect(response).to redirect_to xxxxx_url
 end
end

と書いており、ロジック的にはその通りなんだけど、そもそも「氏名を登録してる場合」ってなんだよ!と思うようになり

context 'ユーザ情報を登録している場合' do
 it '趣味の登録画面に遷移する' do
  expect(response).to redirect_to xxxxx_url
 end
end

に直しました。

他に、投稿記事の公開日を登録するメソッドのテストで

it `published_at に日付が登録されていること`

と書いてたのですが

it '公開日が登録されていること'

とかの方がいいのかなぁ〜🤔など、考えるようになりました。正解は案件ごとにあると思いますが、この表現って伝わる?という視点が加わったことは、個人的にとても有益だなぁと感じます。

ただ、「完璧主義」が出てくると細かい所がずっと気になり・・めっちゃ時間過ぎてる!ということもあったので、少しずつ調整していきたいなぁと思いました。

使いやすさを意識するようになった

TDD の復習として、以前参加した Coderetreat のお題である「ライフゲーム」を TDD で実装しました。

GitHub - dobby618/life_game
※ 記事公開の 2/27 時点でまだライフゲームの実装は途中です。3 月中に終わらせたい。

ライフゲームでは Cell クラスに「生・死」のステータスを持たせており、「生・死」の判定に cell.status を使っていたのですが

class Cell
 attr_accessor :status
 
 def initialize(status:)
  @status = status
 end
end

cell = Cell.new(status: true) # 「生」のステータスを持つセル
if cell.status
 # 「生」のステータスを持つセル
else
 # 「死」のステータスを持つセル
end

TDD で「どんな風に使いたい?」と先に考えることで、「生・死」の判定は ? メソッド使いたいなぁと思い修正しました。

class Cell 
 def initialize(alive:)
  @alive = alive
 end

 def alive?
  @alive
 end

 def dead?
  !@alive
 end
end

cell = Cell.new(alive: true) # 「生」のステータスを持つセル
if cell.alive?
 # 「生」のステータスを持つセル
else
 # 「死」のステータスを持つセル
end

最後に

個人的に

  • 作りやすやよりも、使いやすさ
  • 無駄なテストコードは自分しか消せない

という言葉は大変刺激になりました!

また、t-wada さんの最後の話で TDD を生み出した方でさえ「テスト書きたくない・・」と思うことがあるそうで、そんな時グリーンバンドに刻まれた「acts_as_professional」を見て奮い立たせている!という話を聞いて、私も見える位置に飾ることにしました。

t-wada さんのメッセージ入りです! Run with Tests!

f:id:meikotan:20200226205403j:plain:w150
グリーンバンド

TDDBC に参加したことで、たくさんの新しい視点を得れたことがとても嬉しいです。出来るところから実務に活かし、日々「動作するきれいなコード」を目指します!

また、agile459 の勉強会はいつも業務に直結するものが多く、学びが多いです。開催ありがとうございました!そして、いつも togetter してくれる HAL さん!ありがたいです。私は勉強会に参加しながらの Tweet が苦手なので、皆さんの実況を振り返りに使っています🙏✨
【自分用まとめ】TDD Boot Camp in 愛媛 #1 #tddbc #Agile459 - Togetter