Vapor Trail

明るく楽しく元気よく

TDDBC札幌2019に参加しました

2019.06.15 TDDBC札幌2019「見てわかるテスト駆動開発

基調講演+ライブコーディング

speakerdeck.com

印象に残った部分のメモ

「動作するきれいなコード」をはじめから書くのは難しい
天才ははじめからできるが凡才はどうするか

  • 「分割統治」
  • まず動作するコードを書く
  • その後できれいなコードを書く

TDDのサイクル

  1. 目標を考える
    よくあるTDDの誤解=設計をしない
    でも設計はやりすぎない
    実装をしていくと集中はできるけど視野は狭くなる
    はじめにTODOリストを作る
    コードを書いて初めて分かることもある
    その時は適宜TODOリストを更新する

  2. 目標を示すテストを書く
    例:10個のTODOリスト
    10項目のTODOリストを1つずつ倒していく
    まず弱そうなやつから倒していく
    TDDの1周目は重い
    使う側の視点からコードを書く

  3. そのテストを実行して失敗させる

  4. 目的のコードを書く
    動かすことに集中する
    コピペでも構わない
    この時点では動作する汚いコード

  5. 2で書いたテストを成功させる

  6. テストが通るままでリファクタリングを行う
    リファクタリングはTDDとは独立したテクニック
    動作しているテストが動作しているままで、テストが赤くなった→外部から見た振る舞いが変わったということ

  7. 1~6を繰り返す

大半のコードは動作する汚いコード
きれいにしようとして動かなくなることを恐れる
動いていないことに気づかないことが一番怖い
ソフトウェアで大事なのが動くものを提供すること

TDDのライブコーディングのサンプル FizzBuzz

まずTODOリストでやることを明確にする 構成要素を分解していく
大きいタスクを小さなタスクに分割していく

1から100まで数をプリントする部分は後回しにする 標準出力のテストは難しい
すぐ壊れやすいViewのテストは費用対効果が低い
Viewにロジックがなければテストする必要がない
TDDはテスト容易性の設計
Viewまわりはテストする必要がないようにロジックを書かない
テスト容易性を高めていく

// TODOリスト
[ ] 数を文字列に変換する  
ただし、  
[ ] 3の倍数のときはFizz  
[ ] 5の倍数のときはBuzz  
[ ] 3と5の両方の倍数のときはFizzBuzz    

これらの部分がロジック
TODOリストの作成を通して、表記の揺れをなおす、仕様を明確にする

TDDの一周目が重いのは無から始めるから
ファイル名は何にするか、クラス名はなにするかなど...

テストファイルを作りすぐ実行する
テストコードを書いてからテストを実行するとハマることがある
赤くなる理由は、Unitテストのインストールが上手くいっていない場合などもある
スタートラインに立っていることを確認する

テストメソッド名は日本語でよい
テストコード=ドキュメント
何をテストしているのか明確にわかるようにする

擬似コード
public class test {
  public function test(){
    // 準備
    // 実行
    // 検証
  }
}

検証がゴールなので下から順に書いていく
何をしているのかが明確になる
単一のゴールから書く
DBからデータを取ってあれこれしてと準備のコードが多くなると、まとめてassertをたくさん書いて検証も多くしたくなる

ゴールは期待値 expected
手が動かないときは、TODOリストの抽象度を下げる
具体化する
「数を文字列に変換する」→「1を渡すと文字列"1"に変換する」

// TODOリスト
[ ] 数を文字列に変換する  
  [ ] 1を渡すと文字列"1"に変換する    
ただし、  
[ ] 3の倍数のときはFizz  
[ ] 5の倍数のときはBuzz  
[ ] 3と5の両方の倍数のときはFizzBuzz  
// 準備
// 実行
クラス名を考える
String actual = fizzBuzz.stringfy(1);
// 検証
assertEquals("1", actual)

作る前に使ってみる
使いやすいかは使わないとわからない
使いやすさにフォーカスする

// 準備
FizzBuzz fizzbuzz = new FizzBuzz();
// 実行
クラス名を考える
String actual = fizzBuzz.stringfy(1);
// 検証
assertEquals("1", actual)
public class FizzBuzz {
    public String stringfy(int i){
        return "1"
    }
}

コンパイルエラーから進歩した
TDDの1週目は考えることが多くて重いので小さめのタスクから書く グリーンにする
  

// 準備
FizzBuzz fizzbuzz = new FizzBuzz();
// 実行&検証
assertEquals("1", fizzBuzz.stringfy(1))

リファクタリングに着手する テストコードもリファクタリングする

// TODOリスト
[ ] 数を文字列に変換する  
  [x] 1を渡すと文字列"1"に変換する  
  [ ] 2を渡すと文字列"2"に変換する  
assertEquals("2", fizzBuzz.stringfy(2))
assertEquals("1", fizzBuzz.stringfy(1))
assertEquals("1", fizzBuzz.stringfy(1))
assertEquals("1", fizzBuzz.stringfy(1))
assertEquals("1", fizzBuzz.stringfy(1))
assertEquals("1", fizzBuzz.stringfy(1))
assertEquals("1", fizzBuzz.stringfy(1))

複数のアサーションを書くと、失敗したアサーションで終了してしまい、後のアサーションは実行されない
1つのテスト関数に複数のアサーションがあることをアサーションルーレットという
どのアサーションが失敗したのかがわからない
何のテストをしているのかがわからない
テストごとに分離性を高める

  • テストのアンチパターン
    テストコードは上から順番には実行されない
    →依存関係を減らすため
    テスト間に依存関係を作らない
    単体で完結するようにする
    テストを分散並列実行できるようにする
// TODOリスト
[ ] 数を文字列に変換する  
  [ ] 1を渡すと文字列"1"に変換する →仮実装  
  [ ] 2を渡すと文字列"2"に変換する →三角測量    
ただし、  
[ ] 3の倍数のときはFizz 仮実装→実装  
[ ] 5の倍数のときはBuzz 明白な実装  
[ ] 3と5の両方の倍数のときはFizzBuzz  

...3年後 このコードを書いた人が辞めて引き継いだと仮定する
振る舞いはわかるけど仕様はわからない
TODOリストはあくまで個人的な備忘録なので残ってない

・テストコードに仕様を残すやりかた

1.メソッド名に書く

public function __仕様文_テスト名()  

2.ファイル内でクラスに分割する

class 3の倍数のときはFizz {
    function test_xxx {
    }
}

テストする内容を構造化(ツリー化)する
冗長なテスト名を構造化して省く

このときにテストの粒度が一定ではないことに気づく

// TODOリスト
[x] stringfyメソッドはintを文字列に変換する  
  [x] その他の数のときはその数をそのまま文字列に変換する  
    [x] 1を渡すと文字列"1"に変換する →仮実装  
    [x] 2を渡すと文字列"2"に変換する →三角測量  
    [x] 3の倍数のときはFizz 仮実装→明白な実装  
    [x] 5の倍数のときはBuzz 明白な実装  
[ ] 3と5の両方の倍数のときはFizzBuzz  

・この重複に意味はあるのか
前任者は深い意味がないことを知っている
1を渡すと文字列"1"に変換する
2を渡すと文字列"2"に変換する(三角測量のためだけに作った)

1と2に論理的な重複がある
引き継いだ人はテストコードを消す度胸はない

テストコードを最小にするのが保守としては楽

品質保証の点からはもっとテストを書く
境界値などのテストを書くと良い
6などの適当に選んだ数字ではなく101などにする

質問

  • Gitのコミットの粒度について
    細かくコミットして後でrebaseする
    緑のタイミングでプッシュする

  • コードレビューでどこを見るか
    テストコードの結果を見てまずレビューする

  • DBは本物を使うか。モックを使うか。
    基本的に本物を使う
    モックを使うとモックのメンテに疲れる
    fukabori.fmでも話されている

fukabori.fm

『日本語の作文技術』
最近たまたま買って読んでいるので、 個人的な感想を書くと読み手にとってわかりやすい文章を書くことにフォーカスした本で、 ドキュメント書いたり、英語のドキュメントを翻訳するときとかに役立つと思います。

感想

TDD本もそうだがライブコーディングを通して TDDで実装していく過程(どこに注目するのかなど)を追体験できることがよかった。

特にViewにロジックを書いてはいけないということはわかっていたけど、 なぜ書いてはいけないのかと聞かれると「わかりにくくなるから」とかその程度の理解だったので、 「Viewをテストすることは難しいのでロジックを分離してテストを容易にする」という無意識でなんとなく理解していた(理解しているつもりであった)部分を言語化できたことは学びであった。

「明白な実装」「三角測量」「将来の読み手を考えたテスト」など、よく考えるとTDD本に書いてあることだと気づいた。 TDDBCに参加した後で再読すると新しい発見があると思うので、もう1度写経する必要があるなと感じた。

後半のペアプロ

TDD Boot Camp(TDDBC) - TDDBC仙台03/課題

第一言語PHPにしたけど、第二言語のGoになってしまった。Go歴1ヶ月のほぼ初心者。
ペアで組んだ方は気さくに話せる優しい方でよかった。

Goはクラスがないので構造体の初期化時にエラーを出す方法とか、そもそも文法がわからなくて悩んだ。
自分の場合、業務では設計とか実装でわからない部分があると、後で修正する大変さを考えてしまって、 どうするのがベストなのか悩んでしまって手を動かさないまま時間を浪費することが多い。
しかし、TDDで実装を進めると後で修正できるという保険があるので推進力が生まれると感じた。 ペアプロをして、この書き方でいいのかと悩みながらも手を動かしながらグイグイ進んでいく感がよかった。

引数だけ変えてテストするときの冗長を回避する「パラメータライズドテスト」を知った。
PHPUnitにデータプロバイダがある
https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html

全体通して

学びが多かった。

自分で参加登録したくせに、いつもの如く前日から不安になってきて、 当日の朝に「体調不良で休もうかな...」とか思ってましたが参加してよかったです。
スタッフや講師の方のおかげだと思いますが、会場の雰囲気的に心理的安全姓が高かったと思う。

懇親会ではt_wadaさんとお話する機会があって、お聞きしたかったことや個人的に悩んでいたことを話せて、他の方もアドバイスをしてくださったりしてよかったです。

TDDBC 札幌を運営してくださったスタッフの皆様、参加者、講師のt_wadaさんありがとうございました!