📖

「テスト駆動開発」読書メモ

2024/08/15に公開

書籍

「テスト駆動開発」
著者:Kent Beck
訳:和田 卓人
出版年:2017年
出版社:オーム社
リンク:https://www.amazon.co.jp/テスト駆動開発-Kent-Beck/dp/4274217884

概要

以前会社の目標のために「テスト駆動開発」を読み、社内で共有しました。
以下の内容は、社内で共有する際のアウトプットとなります。

読んだ背景

現場にテスト観点表を作るにあたり、そもそもテストの運用・手法を見直してみようと思い、本を読んでみました。
また、現場がスクラム開発をしており、ペアプロやモブプロと共に、TDDがおすすめプラクティスであったことも理由です。

感想

読む前は「先に単体テスト書いていく開発方式なんだろうな」くらいの認識でしたが、だいぶ認識を破壊されてすごい面白かったです。

特に、最初の方で「期待値が10」だったら、プロダクションコードの方でべた書きで「return 10」でやってみて、必要に応じて直していくと書いており、「そのレベルからやってもいいんだ」と勉強になりました。

結果的には、現場で仕様が決まり切っていない中開発を進めることがあるため、チームとして導入することはできませんでした。
しかし、TDDは個人レベルで取り入れられる開発手法であるため、私はたまにリファクタが行き詰った際にはTDDの考え方でリファクタしたりしています。

本書は要約でもよいかもしれないですが、読む前の自分のようにTDDを全く知らない状態であれば、価値観をまっさらにして読むと面白いと思います。

読書メモ

第Ⅰ部: 他国通貨

テスト駆動開発の過程は以下のとおりである。

  • まずはテストを1つ書く
  • すべてのテストを走らせ、新しいテストの失敗を確認する
  • 小さな変更を行う
  • すべてのテストを走らせ、すべて成功することを確認する
  • リファクタリングを行って重複を除去する

そこでは、以下のような課題が出てくる。

  • 少しずつ増えていく機能を各テストがどうやって支えるか
  • いかに小さく格好悪い変更で、新たなテストを通すか
  • いかに頻繁にテストを走らせるか
  • いかに小さいステップを踏んでリファクタリングを行うか

第Ⅰ部では、債券ポートフォリオの実装での話を扱う。

元々、ドルのみの機能であったところで、複数通貨扱えるようにする必要がある。
実装では、不備がないかどうか、どのように確認しつつコーディングするかどうかが重要となる。

第1章: 仮実装

複数通貨を扱う場合、レートの計算が必要となる。
レートの計算では、掛け算と足し算が必要となる。
掛け算が出来なければ足し算できないため、掛け算にフォーカスした単体テストをまず実装する。

例えば、PHPUnitなら以下のようになる。

class CalcTest
{
    public function testCalc()
    {
        $calc = new Calc();
        $result = $calc->calc();
        $this->assertEquals($result, 10);
    }
}

上記のテストはもちろん失敗するため、以下のようにテストを通過するためのクラスを作る。

class Calc
{
    public function calc()
    {
        $num = 10;
        return $num;
    }
}

無駄な作業に感じられるかもしれないが、テスト駆動開発では、ここまで細かいステップを踏み続けられることが重要となる。
細かいステップを踏むことで、もれなくテストを実装することが出来る。

第2章: 明白な実装

テスト駆動開発は、以下の2パターンで実装できる。

  • 仮実装:コードにべた書きで値を記載し、徐々に変数に置き換えていく。
  • 明白な実装:すぐに頭の中の実装をコードに落とす。

実装内容が明白な場合は、「明白な実装→明白な実装」のように続いていく。

しかし、予期せずテストが落ちる場合は、すぐに「仮実装モード→リファクタリング」のように実装方法を変える。

第3章:三角測量

あるオブジェクトのインスタンスを作成し、その後にそのインスタンスを使って別のインスタンスを作成して値を変更した場合、元々作成していたインスタンスの値が変わってしまうことがある。

その場合、値オブジェクトを利用することで、値を変更する場合は完全に別のインスタンスを作成する必要があるため、別名参照問題を回避できる。

べた書きでテストを書いた場合、そのテストがたまたま通った可能性を消すことが出来ない。

そのため、2種類以上の値でテスト(三角測量)するようにする。

第4章: 意図を語るテスト

テストを見て、オブジェクトが何を返すのかわかりやすく修正する必要がある。

また、出来る限りプロパティなどのアクセス修飾子は小さくする。

第5章: 原則をあえて破るとき

構造が同じ作りであれば、「テストを書く→テスト実行する~」の過程を飛ばしてコピー&ペーストすることで、速度を向上させることが出来る。

ただし、コピー&ペーストすることで重複が生まれるため、重複の排除をすることは必須となる。

第6章: テスト不足に気づいたら

第5章のように、コピー&ペーストしたコードは、きれいにする必要がある。

その場合、コピー元を継承するように修正した場合は、コード量がほとんど減らない。

しかし、新たに共通の親クラスを作り、それを継承することでコードを削減することが出来る。

コピー&ペーストした場合や、既存のテストを修正する場合などは、テストが不足している場合がある。

その場合は都度テストのアサーションメソッドを追加することで、コードが壊れることを防ぐことが出来る。

第7章: 疑念をテストに翻訳する

第6章にて、クラスを共通化した場合、クラスが一致しない場合も同一として判定されてしまう。

そのような場合、getClassメソッドを利用して「クラスが一致しているかどうか」の観点のテストを、同一かどうかのテストの観点に含めることで比較できる。

第8章: 実装を隠す

2つのサブクラスのメソッドの処理が似ている場合、親クラスに共通メソッドを作成する。

移行の前段階として、ファクトリメソッドを使うようにテストコードを書き換える。

その後、共通メソッドを親クラスに作成することで、サブクラスの実装を隠す。

第9章: 歩幅の調整

2つのサブクラスで定義している変数を、ファクトリメソッドのコンストラクタに移動させた場合、共通の実装をすることができる。

その際、別メソッドのコンパイルエラーとなる可能性がある。

しかし、原則として、可能な限り小さなステップを踏むことが求められるため、その修正は後で実施する。

ただ、そこまで細かいステップを踏む場合、時間がかかってしまい現実的に難しいため、行き詰らないのであれば以上のステップを踏まなくてもよい。

第10章: テストに聞いてみる

第9章までで、サブクラスのメソッドを親クラスで共通化させる変更を行なった。

この共通化が本当に正しいのかどうかについては、テストが通るかどうかで判断するのがよい。

共通化の過程でテストが落ちた場合、変更を巻き戻してからテストを追加し、それを通過するようにテストを実施する。

その過程によって、リファクタリングした際に、共通化処理が正しい状態であることが担保される。

第11章: 不要になったら消す

共通化が出来たら、元々のメソッドを削除する。

また、元々のメソッドを検証していたテストも過剰なテストとなるため、削除する。

テストを削除することに関しては悪ではないため、リファクタリングをした際などテストが過剰になってしまう場合は、テストを削除する。

第12章: 設計とメタファー

新規要件により、期待通りの振る舞いにできない場合、Imposterパターンを利用し、同じプロトコルを持つ新たなオブジェクトを実装することが出来る。

例えば、「為替を考慮し、複数の通貨を扱いたいが、1つの通貨を扱っているような振る舞いで実装する」ようにしたい場合、以下のように処理を分けることが出来る。

  • Moneyオブジェクト:共通的な値を計算結果として返す
  • Expressionオブジェクト(imposter):値を指定の通貨に変換して表現する

以上のように、役割を分けることで、同じような振る舞いで実装することが出来る。

第13章: 実装を導くテスト

第12章で仮実装をしたメソッドを本実装する場合、Expressionをインターフェースとして、以下のオブジェクトでロジックを実装する。

  • Sumオブジェクト:足し算をする(reduceメソッド)
  • Bankオブジェクト:sumを利用して計算した値を返す

2つ以上のクラスにて、同じメソッドを実装する場合は、ポリモーフィズムを利用するようにする。

第14章: 学習用テストと回帰テスト

新しいメソッドを追加した場合にAPIが動くかどうかなど、わからない場合は仮のテストを作成して検証する方法がある。

これを学習用テストという。

テストが落ちた場合に、不具合を再現させるテストを作成し、不具合修正ができたかどうかをそのテストの結果で判断することができる。

不具合を再現させるテストを回帰テストという。

第15章: テスト任せとコンパイラ任せ

1つ修正を行った場合、連続して複数の修正を行わなければならない場合がある。

その場合、以下の2つの方法で正しいかどうか判断できる。

  • テスト任せ:より具体的なテストを作成し、一般化してテストがOKになれば正しいと判断する。
  • コンパイラ任せ:コンパイルエラーにならなければ、正しいと判断する。

まずは理想の状態のテストを作成する。

変更ごとにコンパイルエラーが起きるため、その解消に注力し、エラー解消後にテストが通るよう修正する。

そうすると、変更を行なって、コンパイルエラー解消とテストがOKになることをもって完了とできる。

第16章: 将来の読み手を考えたテスト

テストを書く上で、あえて変数宣言した方が良い場合は変数宣言する。

変数名によって、テストの意図を将来の読み手に伝えることが出来る。

また、テストの内容は、実装の詳細をなるべくテストしないようにする。

テストは、オブジェクトを外部から見た振る舞いにフォーカスして作成するのが良い。

第17章: 他国通貨の全体ふりかえり

「テストが足りているか」という観点については、「こう動いてはならない」というテストを作成し、そのテストが期待通りになるかどうか判断するのが良い。

ステートメントカバレッジ(命令網羅率)は、品質の保証にはならないが、TDDを忠実に実施した場合、100%となるため、TDDのスタート地点にはなる。

テストで担保されているかのチェックとして、欠陥挿入という方法がある。

コードの任意の行の意味合いを変更し、テストが落ちれば、テストが担保されていると判断できる。

第Ⅱ部 xUnit

第18章: xUnitへ向かう小さな一歩

TDDを習得したら、より大きな機能追加をテストを書きながら行えるようになる。

しかし、TDDをマスターする過程で、いつでも小さな手順に引き戻せるようでなければならない。

小さなステップを飛ばして実装し、のちに動くはずのコードが動かなかった場合、無駄になってしまう。

第19章: 前準備

テストを書く際に、以下のような基本パターンがある。

  • 準備(Arrange): オブジェクトを作る。
  • 実行(Act): そのオブジェクトに対して操作を行う。
  • アサート(Assert): 結果の検証を行う。

準備に関しては、テスト間で重複することがあるが、実行・アサートについては重複しないことが多い。

準備に関して、オブジェクトを何回作成しなければならないかについて、以下の2つの相反する制約がある。

  • パフォーマンス: 別々のテストだが、オブジェクトを使い回せれば、早くテストが動作する。
  • 独立性: テストの成功/失敗は、他のテストの結果に影響してほしくない。テスト間で共有されているオブジェクトの値が変更されてしまう場合、他のテストに影響する可能性がある。

テスト同士で依存関係を作らないために、独立性を重視する必要がある。

そのために、setUpメソッドを使い、テストケースをシンプルにする。

第20章: 後片付け

※Laravelでは、RefreshDatabaseを使った場合は考慮不要。

seUpで作成したデータを削除するため、tearDownメソッドを使う必要がある。

tearDownをフラグで管理した場合、管理が大変になるため、ログで管理するようにする必要がある。

第21章: 数え上げ

※Laravelでは、RefreshDatabaseを使った場合は考慮不要。

例外が出てもtearDownを実行できるようにする必要がある。

全てのテストの例外をキャッチするのはハードルが高いため、testResultを利用し、テストの情報を返すようにする。

testResultにより、テスト実行時のカウントを取得できるようになった。

第22章: 失敗の扱い

大きなテストが落ちてしまった場合、小さなテストを書いてそれを通したのちに大きなテストを復活させる。

その手順を踏まえることで、大きなテストが通るような実装をすることが出来る。

第23章: スイートにまとめる

テストが重複してしまう場合、Compositeパターンを利用し、テストをまとめる。

その際、Collection Parameterパターンを利用することで、返り値をベースにオブジェクトを利用するのではなく、パラメータを基にオブジェクトを利用することができる。

第24章: xUnitの全体ふりかえり

テストは、自分自身で実装することが重要である。

自分自身で実装することによって、自分が一番よく知っている道具が手に入る。

また、新しい言語を触るときは、xUnitを実装してみる。

テストが10個程度動作するようになる時には、日々のプログラミングに必要な機能は登場している。

第Ⅲ部: テスト駆動開発のパターン

第25章: テスト駆動開発のパターン

テスト

ソフトウェアのテストでは、自動テストを書く。

ストレスがかかると、以下のような悪循環が起き、テスト実行が減ることとなる。

  1. ストレスがかかるとテスト実行が減る。
  2. エラーが増える。
  3. ストレスがかかる。
  4. 1.へ(無限ループ)

急いでいるときなどストレスがかかるような状況でも、自動テストがあることで、不安を抑えることが出来る。

独立したテスト

テストの実行は、絶対にほかのテストに影響を及ぼすべきではない。
凝集度が高く、低結合のテストを作ることで、パフォーマンスについても担保される。

TODOリスト

コードを書く際は、何をすべきかわからない場合、何も書かないようにする。
やるべきかもしれないことが増えると、いま何をやっているのか見失いやすくなる。

新しく何かを考え付いた場合、「すぐやる」「あとで」のリストに加えるか、まったくやる必要がないか判断すればいい。

テストコードを一気に書いてしまう場合、既に書かれているテストのリファクタリングや、10個一度に修正する場合などで、面倒に感じてしまう。

そのため、テストコードは一気に書かないようにする。

テストファースト

テストは、テスト対象のコードを書く前に記載する。

そうすることで、コードを実装した後にテストするストレスを減らすことが出来る。

アサートファースト

アサーションは、最初に書くようにする。

「アサートメソッド→テスト→機能→システム」の順に実装することで、アサーションがシンプルになる。

テストデータ

なるべく、後で見た人が分かりやすい変数や値のテストデータを作成する。

可能な限り、本番に近いデータを作成するようにする。

明示的なデータ

アサーションの中に式を書くことで、コードで何が必要か明白になる。

また、読み手に分かりやすくなる場合においては、マジックナンバーを使ってもよい。

第26章: レッドバーのパターン

を示すテスト

分かり切ってはいないが、書けば動きそうなテストを書く。
書いて動かせれば、未知の状態から既知の状態となる。

はじめのテスト

はじめは、そこから学ぶことがありそうで、すぐに書けそうなテストを書く。
既に何回か実装したアプリケーションを再実装している場合には、機能が1つか2つテストを書くようにする。

説明的なテスト

他の人と、テストコードの形で知識を共有するようにする。

学習用テスト

サードパーティツールなど、どのように動作するかわからない機能は、まずテストを書いて確かめるようにする。

そのテストを学習用テストという。

脱線はTODOリストへ

脱線しそうになったら、TODOリストに書き加えて脱線しないようにする。

新しいアイデアに心惹かれても、本来の仕事に割り込ませないようにする。

回帰テスト

不具合が報告されたとき、不具合を再現させるテストを行い、それを通すために修正を行う。
そのテストを回帰テストという。

休憩

疲れたり、手詰まりになった場合は、休憩を取るようにする。

席を立った途端にアイデアを思いつくことがある。

疲れれば疲れるほど、疲労事態に気づきにくくなり、もっと作業を続け、さらに疲労してしまう。

アイデアが出ない場合には、以下の単位で休憩を入れることを検討する。

  • 時間単位:飲み物をおいておけば、生理現象として、定期的に休憩したくなる
  • 日単位:定時後に予定を入れておけば、残業に突入することなく、一晩おいて考えられる
  • 週単位:週末に予定を入れておけば、気がかりで消耗するような悩みを吹き飛ばすきっかけになる
  • 年単位:強制的バケーション制度は完全なリフレッシュ効果を発揮する

やり直す

手詰まりの場合は、コードをいったん捨ててやり直す。
ペアプロの場合は、交代のタイミングが作業やり直しのタイミングとなる。

安い机に良い椅子

机などほかの家具はケチっても、椅子はいいものを買う。

第27章: テスティングのパターン

小さいテスト

大きいテストが失敗した場合、問題を抽出して小さいテストを書いてみる。

小さいテストが通ったら大きいテストに戻る。

Mock Object(擬装オブジェクト)パターン

構築処理が重かったり、準備に手間取るリソースは、Mockに置き換えることで高速化できる。

本物のデータベースは、なるべく使わないようにすることで、テストを高速化できる。
Mock Objectを使うことで、オブジェクトの可視性に留意するようになるため、コードの結合度を下げることが出来る。

Self Shunt(自己接続)パターン

オブジェクトが、ほかのオブジェクトときちんとやり取りしているかのテストをする際に、Self Shuntパターンを使う。

Self Shuntパターンを使うことで、テスト対象オブジェクトが本物だと思って話している相手が、実はテストケース自身となるようなテストを作成する。

Log String(記録用文字列)パターン

正しい順序でメソッドが呼び出されていることをテストしたい場合、記録用の文字列を作り、メソッド呼び出しのたびに文字列に追記するようにする。

その文字列を比較することでテストする。

Crash Test Dummy(衝突実験ダミー人形)パターン

普通は到達しない、エラー処理部分のコードをテストする場合、普通のテストを実行し、例外を発生させるような特別なオブジェクトを使うようにする。

Mock Objectパターンに似ているが、オブジェクト全体を擬装する必要はなく、対象メソッドだけを上書きして例外を再現させることが出来る。

失敗させたままのテスト

独りでプログラミングしているときのコーディング時間の終わらせ方は、テストを失敗させたままにして終了させる。

そうすることで、書きかけの内容を見て、そのとき何をしていたのか思い出すことが出来る。

きれいなチェックイン

チームでプログラミングしているとき、コーディング時間はテストをすべて通る状態にして終了させる。

チームの場合、最後に見たときからコードがどのように変わっているかわからない場合が多い。

また、コードをチェックインする前に、テストがすべてOKとなることを確認する。

テストスイートで失敗するような場面では、最もシンプルなルールとして、「手元の作業を捨てて初めからやり直し」をする。

そのルールを導入することで、なるべく早くチェックインしようという空気が生まれる。

テストを通すためにテストをコメントアウトすることは禁止とすべき。

第28章: グリーンバーのパターン

仮実装を経て本実装へ

べた書きの値を仮実装して、そののちに本実装で修正する。

仮実装コードは、本実装の際のリファクタリングで取り除くため、「無駄なコードを書かない」ルールには反しない。

三角測量

テストから最も慎重に一般化を引き出すやり方として三角測量がある。

三角測量では、テストを2つ以上に増やし、そのテストを通るようにすることで一般化できる。

ただ、テストを追加して一般化できたことに対してテストが必要となり、無限ループとなってしまう可能性があるため、一般化の方向が分かっている場合には、仮実装または明白な実装をするのがよい。

明白な実装

シンプルな実装など、実装内容が頭に浮かんでいる場合は、明白な実装を行う。

ただ、明白な実装をしている中で行き詰った場合は、仮実装や三角測量を行い、テストを通すレベルに下げて実装するのが良い。

一から多へ

オブジェクトのコレクションを扱う操作を実装する場合、まずは単数の操作を実装し、それからコレクションでも動くようにする。

第29章: xUnitのパターン

アサーション

テストが動作しているかどうかの判断は、真偽値で行う。

真偽値の判断はコンピュータが行う。

真偽値については、等価性の判定や具体的な値での判定など、細かく判定する。

また、publicなメソッドのみテストするようにする。

フィクスチャー

複数のテストから使われる共通のオブジェクトを作る場合、setUpメソッドで初期化を行う。

メリットとしては、共通処理をまとめられる点だが、デメリットとしてはsetUpの処理を覚えてテストを実装しなければならない。

外部フィクスチャー

フィクスチャーとして作成した外部リソースを解放するとき、tearDownを使う。

テストメソッド

テストケースは、「test」で始まるメソッドに記載する。
テストメソッドの中は、平易で読みやすく書かなければならない。

テストメソッドが長くなってきた場合、最終ゴールのための一歩を示す最小のテストメソッドを書くようにし、テストメソッドの長さは3行を目指す。

テストやコードを実装する際、コメントでアウトラインを書くようにし、その後に具体的な実装をするとよい。

例外のテスト

例外発生を期待するテストの場合、期待される例外をキャッチして握りつぶすように書き、その例外が発生しなかった時だけテストが失敗するように実装する。

このようにすれば、期待していなかった種類の例外の場合も、テストを失敗させることが出来る。

まとめてテスト

テストをまとめて実施したい場合、すべてのテストスイートをまとめたスイートを作り、実行する。

第30章: デザインパターン

問題自体がいかに多様であり異なる背景を持っていようとも、実は問題は一般的であり、その下位も一般的であると期待できる。

TDDでは、以下のデザインパターンを利用する。

  • Command: 処理の実行をただのメッセージではなくオブジェクトで表現する。
  • Value Object: 一度作られたら絶対に値が変わらないオブジェクトを作り、別名参照問題を防ぐ。
  • Null Object: 特殊な状況をオブジェクトで表現する。
  • Template Method: 処理の順序を抽象メソッドの並びで表現し、個別の処理は継承によって実現する。
  • Pluggable Object: 2種類以上の実装を持つオブジェクトを呼び出すことでバリエーションを表現。
  • Pluggable Selector: インスタンスごとに異なるメソッドを動的に呼び出すことで、余計なサブクラスを作らずに済ませる。
  • Factory Method: コンストラクタではなくメソッドを呼び出してオブジェクトを作成する。
  • Imposter: 既存プロトコルの新たな実装を作成してバリエーションを生み出す。
  • Composite: オブジェクトたちの振る舞いの組み合わせを1つのオブジェクトとして表現する。
  • Collection Parameter: さまざまなオブジェクトから処理結果を集めるためのオブジェクトを引数に渡していく。

Commandパターン

処理の呼び出しが、シンプルなメソッド呼び出しよりも複雑になってきたときは、処理のためのオブジェクトを作成し、それを起動するようにする。
例えば、runメソッドに内容を記載し、それを起動するようにする。

Value Objectパターン

広く共有されるものの、同一インスタンスであることはさほど重要ではないオブジェクトを設計する場合、以下のようにする。

オブジェクト作成時に状態を設定したら、その後は決して変えないようにする。

オブジェクト操作は、必ず新しいオブジェクトを返すようにする。

以上のようにすると、インスタンス生成後に値が書き換えられる危険(別名参照問題)がなくなる。

Null Objectパターン

特殊な状況をオブジェクトで表現したい場合、その特殊な状況を表現するオブジェクトを作り、通常のオブジェクトと同じメソッド群を実装する。

NULLチェックをする場合は、メソッド1つ1つチェックするのは漏れが出る可能性があるため、NULLが入ってきた時用のメソッドを用意する。

Template Methodパターン

処理の順序だけ規定し、拡張は将来に向けて開かれた状態にしたい場合、他のメソッドを順番に呼び出すだけのメソッドを書くのが良い。

例えば、以下のような順番で呼び出すメソッドを用意するようにする。

  • 入力・処理・出力
  • メッセージの送信・応答の受信
  • コマンドの読み込み・結果の返却

Pluggable Objectパターン

バリエーションを表現したい場合、最もシンプルなのは、明示的な条件分岐を使う方法だが、条件分岐が増殖する懸念がある。

そのため、条件分岐に応じたオブジェクトを作成し、それを呼び出すようにする。

Pluggable Selectorパターン

インスタンスごとに異なるメソッドを動的に呼び出すことで、余計なサブクラスを作らずに実装できる。

しかし、Pluggable Selectorパターンは濫用される危険性があり、どのメソッドが起動されるのかコードから追いにくくなってしまう。

Factory Methodパターン

オブジェクト作成に柔軟性をもたせたいときは、単にコンストラクタを作るのではなく、メソッドを使ってオブジェクトを作成する。

Imposterパターン

処理に新しいバリエーションを導入したい場合、既存オブジェクトと同じプロトコルを備え、実装は異なる新たなオブジェクトを作る。

Compositeパターン

オブジェクトたちの振る舞いを組み合わせた振る舞いを持つオブジェクトを実装するには、構成要素をまとめた Imposter を作る。

Collecting Parameterパターン

たくさんのオブジェクトたちの処理結果を集めるには、処理のパラメータに結果格納用のオブジェクトを渡す。

返り値ではなく、パラメータを基にデータを構築する。

第31章: リファクタリング

差異をなくす

よく似たコードを共通化するには、内容を近づけていき、一致したら一つにする。

ループ構造、条件分岐の中身、メソッド、クラス、などで似ているものを共通化する。

変更の分離

複数の部分から構成されるメソッドあるいはオブジェクトを変更するには、はじめに、変更すべき部分を分離独立させる。

変更箇所を分離独立させた後に実際の変更を行うことで、分離を元に戻せるようにもなる。

データ構造の変更

データの持ち方を変えるには、データを一時的に複製する。

方法は以下のとおりである。

・内部構造を書き換えたあと、外部インタフェースを変更するやり方

  • 新構造のためのインスタンス変数を定義する。
  • 旧構造でデータが設定されている部分をその変数に置き換える。
  • 旧構造のデータを使っている部分をその変数に置き換える。
  • 旧構造のコードを消す。
  • 外部インタフェースに新構造を反映する。

・API 側から変更を行いたい場合

  • 新構造のパラメータを追加する。
  • 新構造のパラメータを内部で旧構造に変換する。
  • 旧構造のパラメータを削除する。
  • 旧構造を使っている部分を新構造に置き換えていく。
  • 旧構造のコードを削除する。

上記の方法は、例えば、単一のテストをコレクションのテストに置き換える場合などで使うことが出来る。

メソッドの抽出

込み入った長いメソッドを読みやすくするには、メソッドの一部分を別メソッドに分離し、そのメソッドを読み出すようにする。

方法は以下のとおりである。

  • メソッドの中から、新しいメソッドとして切り出す意味のある部分を探す
  • ループの中身や、ループ全体、条件分岐の各分岐などがよくある抽出対象である。
  • 抽出する範囲の外で、一時変数への代入が行われていないことを確認する。
  • 旧メソッドから該当範囲のコードをコピーし、コンパイルする。
  • 旧メソッドの一時変数やパラメータの中で新メソッドから使うものを、新メソッドのパラメータに追加する。
  • 旧メソッドの中から新メソッドを読み出す。

メソッドの抽出を行うことで、込み入ったコードを理解したいときに、名前がつけられるため読みやすくなる。

メソッドのインライン化

ねじれたり散らかったりしてしまった制御フローをシンプルにするには、メソッド呼び出し部分をメソッドそのもので置き換える。

方法は以下のとおりである。

  • 対象のメソッドをコピーする。
  • メソッド呼び出し部分にそのメソッド本文をペーストする。
  • 仮引数を実引数に置き換える。
  • 抽象層をインライン化することで、何が実際に行われているのかを確認した後に、実際の状況に合わせて再度抽象化を行うことが出来る。

インタフェースの抽出

Javaで、ある処理の実装をもう1種類作るときには、インタフェース( interface ) を作り、共通の処理をくくり出す。

方法は以下のとおりである。

  • インタフェースを宣言する。
    ときには既存クラスの名前を使いたいこともある。そのようなときは、先にクラスの改名を行う。

  • そのインタフェースを既存クラスが実装するようにする。

  • 必要なメソッドをインタフェースに加え、クラスのほうでは必要に応じてメソッドの可視性を上げる。

  • コードの中で宣言される型を可能な限りクラスからインタフェースに書き換える。

インタフェースを抽出することで、共通の処理を明示できる。

メソッドの移動

メソッドをふさわしい場所に移動するには、あるべきところにメソッドを加え、それを呼び出すようにする。

方法は以下のとおりである。

  • メソッドをコピーする。
  • 移動先クラスにペーストし、名前を整え、コンパイルする。
  • 移動元のオブジェクトがメソッド内で参照されている場合、移動元オブジェクトをメソッドのパラメータに加える。
  • 移動元オブジェクトの変数がメソッド内で参照されている場合も、それをパラメータに追加する。
  • メソッド内でフィールドへの代入が行われている場合には、リファクタリングを諦める。
  • 移動元クラスのメソッドの中身を新しいメソッドの呼び出しに置き換える。

「メソッドの移動」リファクタリングの長所は、以下の3つである。

  • 対象コードの意味を深く捉えなくとも、メソッド移動の必要があることは容易に分かる。
    1つのオブジェクトに対して、2つ以上のメソッド呼び出しが行われているのは、移動のサインである。

  • 素早く安全に行う手順が存在する。

  • 目を見張るような結果になることが多い。

メソッドオブジェクト

複数のパラメータやローカル変数を必要とする込み入ったメソッドを表現するには、メソッドをオブジェクトとしてくくり出す。

方法は以下のとおりである。

  • オブジェクトを作り、メソッドと同じパラメータを保持させる。
  • ローカル変数は、そのままオブジェクトのインスタンス変数として表現する。
  • 単一のメソッド run を定義し、その中身は元のメソッドと同一にする。
  • 元のメソッドの中で、オブジェクトをインスタンス化し、 run メソッドを呼び出す。

メソッドオブジェクトは、システムに新しいロジックを導入する準備段階に有用である。

メソッドオブジェクトでは、メソッドの抽出に向かないコードをシンプルにすることにも役立つ。

パラメータの追加

メソッドにパラメータを追加するには、以下の方法ができる。

  • メソッドのインタフェースが定義されている場合は、インタフェースのほうに先にパラメータを追加する。

  • パラメータを追加する。

  • コンパイルエラーを活用して、呼び出し側コードを修正する。
    多くの場合、パラメータの追加は機能拡張の1ステップである。

  • データ構造の変更の1ステップとしてパラメータ追加する場合、まず新しいパラメータを追加し、古いパラメータを置き換え、最後に古いパラメータを消すようにする。

メソッドからコンストラクタへのパラメータの移動

メソッドからコンストラクタへのパラメータを移動するには、以下の方法がある。

  • コンストラクタへパラメータを追加する。
  • そのパラメータと同名インスタンス変数を定義する。
  • コンストラクタ内でインスタンス変数への代入を行う。
  • パラメータ parameter の参照を1つずつ this.parameter へ書き換えていく。
  • パラメータへの参照がなくなったら、メソッドからパラメータを削除し、呼び出し側からも削除する。
  • もう付ける必要がなくなった this を消していく。
  • インスタンス変数をふさわしい名前に変更する。

あるオブジェクトの複数のメソッドに対して同じパラメータを渡している場合は、あらかじめパラメータを一度だけ渡しておくことで、重複を排除し、APIをシンプルにできる。

第32章: TDDを身につける

一歩の大きさはどのくらいか

一歩の大きさについては、以下の問いがある。

  • 各々のテストがどのくらいをカバーすべきか。
  • リファクタリングの過程で中継地点をどのくらい作るか。

答えとしては、どちらもできるようになるのがよい。

どちらにせよ、小さいステップで行うのがよい。

テストしなくてもよいものはあるか

以下は、自分が書いたものであれば、テストすべき対象である。

  • 条件分岐
  • ループ
  • 操作
  • ポリモフィズム

良いテストを見分けることができるか

設計に問題を抱えている場合、テストに以下のような兆候が現れる。

前準備に要するコードが長い
→準備に100行のコードがあったら、分割したほうがいい。

前準備コードの重複
共通の前準備コードが見つからない場合は、オブジェクトの結合度が高くなっているかもしれない。

テスト実行時間が長い
10分以上かかる場合は、テストを削減する必要がある。

脆いテスト
思わぬタイミングで失敗するテストは、アプリケーションのどこかが意外な形で他の部分に影響している可能性がある。

TDD はどのようにフレームワークを導くのか

以下のようなパラドックスがある。

「将来のことを考えずにコードを書くことが、将来そのコードの状況に適応する可能性を広げる」

「今日のために書き、明日のために設計しよう」
→TDDでは、 「明日のためにコードを書き、今日のために設計しよう」となる。

実際の機能開発例は以下のとおりである。

最初の機能は、実装がシンプルですぐに終わる。

2番目の機能は、最初の機能のバリエーション。2つの機能重複部分は1つにまとめられて、差異はメソッドやクラスが分かれる形で現れる。

3番目の機能は、前の2つの機能のバリエーション。既にある共通機能はそのまま。独自ロジックの置き場所は明確そのもの、新しいメソッドあるいはクラスである。

開放閉鎖原則(Open/Closed Principle:オブジェクトは利用に対して開かれていて、修正に対して閉じられているべき)

TDD によって開発されたフレームワークは、発生したバリエーションを正確に表現できる。

バリエーションの導入が速くなれば、TDD は事前設計と見分けがつかなくなる。

予期しないバリエーションが行われても、既存のテストを実行すれば、既存機能を壊していないことがすぐに分かる。

どのくらいのフィードバックが必要か

どのくらいテストを書くべきだろうか。

→どの程度の 平均故障間隔(MTBF:Mean Time Between Failures) を考えるかによる。

もしもペースメーカーを開発しているなら、絶対に失敗できないため、絶対に起こらないと証明できない限り、ありえないような条件や組み合わせのテストでも行う意味がある。

どのようなときにテストを消すべきか

テストの数は多いほうがよいが、2つのテストの間に重複がある場合、2つとの残しておくべきだろうか。

→以下の2つの判断基準がある。

自信:テストを消すことで不安に感じることがあれば、決して削除してはいけない。

コミュニケーション:テストの読み手には異なるシナリオと映るのであれば、消さずにそのままにする。

プログラミング言語や環境は TDD に影響するか

TDDのサイクルが面倒な場合、以下のように大きなステップを踏みたくなる。

1つのテストのカバー範囲を広げる。

リファクタリングの中間ステップを飛ばす。

TDD のサイクルを回しやすいプログラミング言語や環境を使うと、いろいろなことを試すことができる。

開発効率を上げて、よりよい解き方を見つかり、純粋な振り返り(レビューなど)に、より時間を使うことができる。

巨大なシステムをテスト駆動できるか

重複を除去して、小さいオブジェクトを作ることで、独立してテストをすることができる。

TDDの可否は、システムの大きさには関係しない。

アプリケーションレベルのテストで開発を駆動できるか

アプリケーションレベルのテスト駆動開発(ATDD:appliction test-driven development)を実現するためには、以下の課題がある。

フィクスチャーの作成が困難である。

顧客にもテストを書いてもらう必要がでてくるため、顧客に新たな責務が発生する。

TDDは、自分ひとりでできる技術のため、周りに伝えながら、一歩ずつ進めれば有効である。

ATDD は、テストからフィードバックまでが長くなってしまうため、テストを通すまでの時間が長くなる傾向がある。

そのため、結局実装のために、開発者視点のテストも必要となる。

途中から TDD に乗り換えるにはどうすればよいか

テストのことを考えずに書かれたコードは、そもそもテストが書きにくくなっている。

→ロジックを一部切り出して、結果を確かめるくらいしかできない。

テストを書きやすいようにコードを書き直そうとした場合、書き直しても機能を壊していないか確認するためのテストがないため、判断できない。

また、テストがないという理由だけでリファクタした場合、金を生まずに時間を消費するだけであり、持続性のあるプロセスではない。

その解消のために、以下の方法がある。

変更のスコープを狭くする。
→変更される予定がないものは、すぐ修正できると思っても、放っておく。

テストとリファクタリングのデッドロックを解消する。
→ペアプログラミングで慎重に作業する。

システムレベルのテストを行う。

TDD は誰のためのものか

TDDは、「より良いコードを書けば、よりうまくいく」という仮説によって成り立っている。

TDD をやっていると、コードに最初から最後まで責任が持てる。

テストが正確になり、テストスキルが上がれば、システムの振る舞いに対する確信が深まる。

TDD は初期状況に左右されるか

TDDには順序がある。

TDD が微視的な初期状況に左右されるなら、巨視的な結果は予測できないといえる。

TDD とパターンの関係

TDDなどは、模倣したい熟練者と同じ振る舞いを生み出せるような、基本的な規則を求める試みである。

なぜ TDD は機能するのか

結合度が低く、凝集度が高く、欠陥率が低く、メンテナンスコストも低いシステムを作るには、欠陥を減らすことが重要である。

欠陥を見つけてから修正するまでの時間が短ければ、コストは小さく、安上がりになる。

また、簡単に修正できると心理的な負担が減らすことができる。

新規機能の追加も、これまでに蓄積されたバグと新しい欠陥を見分ける必要がなくなる。

名前の由来は

  • 開発:ソフトウェア開発における伝統的なフェーズ主義が弱まってきた。
    →いまは「分析→論理設計→実装→テスト→レビュー→結合→デプロイ」のようになった。

  • 駆動:テストで開発が駆動するためである。
    →昔は「テストファーストプログラミング」であった。

  • テスト:自動化され、具体化された、明確なテスト。ボタンを押せば走り出す。
    →TDDはテスト技法ではなく、分析技法である。

TDD と XP(eXtreme Programming)のプラクティスとの関係

ペアプログラミング:解こうとしている問題に相手の合意が得られないような状況を避けるために有効である。
疲れたら、元気なメンバーに交代できるため、TDDを強化できる。

  • いきいきとした仕事:XPでも、元気なうちに働き、疲れたら休むと提言している。
    →イライラして試行錯誤して、進展が見られなかったら止める。
  • 継続的インテグレーション:テストによって、頻繁なインテグレーションを可能にする。
  • インクリメンタルな設計:テストのために必要なコードだけ書き、重複を除去すれば、現在の要件に最も適応し、かつ将来のストーリーに対する備えとなる設計が自動的に得られる。
  • リファクタリング:テストによって、大きなリファクタリングをするときの自信になる。
  • 継続的デリバリ:TDD のテストがシステムのMTBF(平均故障間隔)を改善するのであれば、顧客に迷惑をかけることなく、頻繁に本番環境にリリースすることができる。

Discussion