レガシーJavaコードの動作を確定させるための鍵となるピンダウン・テスト

著者:Mohamed Taman

20201023

本記事をPDFでダウンロード


※本記事はRefactoring Java, Part 2: Stabilizing your legacy code and technical debtの翻訳です

リファクタリングはコードを改善するプロセスで、その目的は品質を改善し、ソフトウェア製品の変更を容易にすることにあります。リファクタリングを行うと、コードはシンプルになり、コードの行数は開始前よりも少なくなります。コードの行数が少なくなるということは、潜在的なバグの数も少なくなるということです。

リファクタリングの実行によってコードが確実に改善され、シンプルになるようにするには、確かなテストが必要です。この確かなテストこそ、リファクタリングの土台です。適切に定義したテスト・ケースでコードの動作が正しいことを検証できなければ、コードの外部動作が変わっていないことを保証できません。リファクタリングのプロセスとは、コードの外部動作に影響を与えずに、その内部構造を変えることです。

本シリーズの最初の記事「Javaにおけるリファクタリング(パート1):テスト駆動開発でアジャイル開発を促進する」では、ローマ数字の「形」(かた)について紹介し、新しいコードを書くことと、そのコードをリファクタリングすることを交互に繰り返しました。

2回目(今回)と3回目(最終回)の記事では、技術的負債を抱えたレガシー・コードをリファクタリングし、俊敏性を高めることに注目します。現実世界のシナリオをシミュレートした、既存のレガシー・コードから技術的負債を取り除くリファクタリングの練習も行います。そのために、ギルド・ローズ(金箔の薔薇亭)のリファクタリングの「形」を使います。この「形」は、Emily Bache氏が作成してGitHubに投稿したものです。

Bache氏は、ギルド・ローズの「形」についてのブログ投稿(英語)で、以下のように述べています。

「この『形』の基本的なは、皆さんが既存のシステムを管理するために雇われたというものです。そして、顧客は新たな機能を求めています。コードを見てみたところ、新機能を追加する前に少しばかりリファクタリングする必要があることがわかります。そしてリファクタリングの前に、自動テストをいくつか追加する必要があります。」

この「形」に従い、技術的負債だらけのレガシー・コードから始めて、新機能を簡単にコードに追加できるところまでリファクタリングを行うことにしましょう。そのプロセスを通して技術的負債を解消し、俊敏性を取り戻します。

本記事での最初のタスクは、レガシー・コードを確実に理解するためのピンダウン・テストを書くことを通して、レガシー・コードの動作を確定させることです。この手順は、今後のリファクタリングでコードの外部動作が変わらないことを保証するうえで役立ちます。

しかしまずは、技術的負債の問題点について考えてみましょう。

 

技術的負債の問題点

技術的負債とは、問題を含むコードであって、リファクタリングによって改善できるものを指します。技術的負債という言葉には、金銭的負債に似ているという意味が込められています。何かを買うためにお金を借りる場合、借りた金額よりも多い金額を返さなければなりません。つまり、元金に加えて、利子も払います。

質の低いコードを書いたり、最初に自動テストを書かずにコードを書いたりすると、組織は技術的負債を背負うことになります。そして、誰かがどこかの時点でそれまでの利子を払わなければならなくなります。

組織が支払う利子は、必ずしも金銭的なものとは限りません。最大のコストは、技術的な俊敏性が失われることです。ソフトウェアの更新や動作変更を、必要なだけの迅速さで行うことができなくなるからです。技術的な俊敏性が低下すれば、組織のビジネスの俊敏性も低下し、求められるスピードでステークホルダーのニーズを満たせなくなります。

そのため、ここでの目的は、負債を抱えたコードをリファクタリングすることです。技術的な俊敏性とビジネスの俊敏性を向上させるため、時間をかけてコードを修正します。

それでは、ギルド・ローズの「形」のコードを通して、コードの動作を確定させてすばやく機能を追加する方法を見ていきましょう。

 

ややこしいレガシー・コードの動作を確定させる

レガシー・コードでの非常に大きな問題の1つは、他の人が書いたものであることです。多くの場合、レガシー・コードはややこしく、ドキュメントも不十分であるため、まったくわかりません。コードを確実に理解するまで、安全に機能を追加することはできません。変更で何も壊れないと十分に確信していなければならないからです。どうすればその確信を得られるかについて確認しましょう。

まず、環境をセットアップします。この「形」を解決するコードは、筆者のGitHubリポジトリにあります。次のようにすると、このリポジトリをクローンすることができます。

~$ git clone https://github.com/mohamed-taman/Agile-Software-Dev-Refactoring.git

本記事のソリューションは、Gilded Roseモジュール以下にあります。本記事のJavaコードは、2つの方法で扱うことができます。

  • 記事に沿って実際に試してみたい方は、ぜひそうしてください。記事の手順にそのまま従ってください。
  • 実際にコードを追ってみたい方のために言うと、本記事は複数の手順に分かれており、それぞれの手順でテスト駆動開発(TDD)の赤-緑-リファクタリングの状態が変わるごとにgitにコミットしています。コードのコミットをたどれば、それぞれの手順の間の差分や、最終的な「形」の要件に向けたリファクタリングによる変更を確認できます。

必要なソフトウェアは、すべて最初の記事「Javaにおけるリファクタリング(パート1):テスト駆動開発でアジャイル開発を促進する」に書かれています。ここでは、最初の記事と同じようにIntelliJ IDEAを使いますが、もちろん同じIDEを使う必要はありません。手順の名前は「A2」で始まっていますが、これは本シリーズの2番目の記事であることを表しています。

手順A2-1:「形」をセットアップします。ギルド・ローズの「形」を使って技術的負債を取り除くために、次のようにして「形」のコードをロードします。

  1. ギルド・ローズのGitHubページに移動し、リポジトリをクローンするか、zipファイルをダウンロードします(筆者は1に示す画面でzipファイルをダウンロードしました)。続いて、ファイル・ブラウザでzipファイルを解凍します。

Downloading the Gilded Rose kata

1ギルド・ローズの「形」のダウンロード

 

  1. 次に、IntelliJ IDEAを起動し、ウェルカム画面で「New Project」リンクをクリックします。次に表示されるパネルの左側で「Maven」を選択し、「Next」をクリックします。プロジェクト名は、Gilded Roseです。「Advanced」を開き、ArtifactIdgilded-roseGroupIdcom.siriusxi.javamag.kataと入力します。「Finish」をクリックします。IntelliJの画面の右下で、「Enable auto-import」をクリックします。
  2. IntelliJのプロジェクト・ブラウザで、Gilded Roseプロジェクト、srcフォルダの順に開き、2のようにmainフォルダとtestフォルダを展開します。

The Gilded Rose kata in IntelliJ IDEA

2IntelliJ IDEAでのギルド・ローズの「形」

 

  1. ファイル・ブラウザに戻り、ビューを列表示に切り替えます。解凍したGilded Roseフォルダで、Javaサブフォルダを探します。
    1. srcフォルダ、mainフォルダ、javaフォルダの順にクリックします。comをIntelliJのsrc→main→javaフォルダにドラッグ・アンド・ドロップします。このままで問題ないので、「OK」をクリックして確定します。
    2. srcフォルダに戻り、testjavaの順にクリックします。comをIntelliJのsrc→test→javaフォルダにドラッグ・アンド・ドロップします。「OK」をクリックして確定します。
  2. IntelliJのプロジェクト・ブラウザでcom.gildedroseパッケージを開き、GildedRoseTest.javaファイルをダブルクリックして開きます。赤い部分が多いことに注目してください。これは、GitHubリポジトリから取得したばかりのギルド・ローズが、IntelliJ向けにセットアップされていないためです。この点を修正しましょう。
    • カーソルを9行目の@Testの上に置き、[Option]+[Enter]キーを押します。「Add JUnit5.4 to the classpath」を選択します。赤い部分がなくなるはずです。
    1. このコードには、foo()メソッドがあります。これはJUnitのタスクなので、このテストを実行しましょう。GildedRoseTestソース・ファイルを右クリックし、メニューから「Run 'GildedRosetest'」を選択します。赤くなります。GildedRoseTestには、失敗したテストが含まれています。
    2. テストが失敗し、赤く表示されても問題はありません。これは、赤-緑-リファクタリングというTDDにおける3つのステップの1つ目です。このテストが失敗するのは、実際に返される値がfooであるにもかかわらず、期待値がfixmeになっているからです。このテストは、すべてが正しくセットアップされているかどうかを確認できるようにするため、意図的に失敗して赤くなるようになっています。
    3. 14行目の文字列をfooに変更します。テストを再実行すると、合格してすべて緑の状態になります。

細かなリファクタリングを行う:ここまでで、赤と緑を確認しました。次に、テスト・コードを改善しましょう。このテストで行っていることがわかりやすくなるように、foo()テスト・メソッドの名前を変更します。このテストは、テスト・フレームワークが起動して実行されていることを確認するものです。テスト・メソッドの名前をjunitFrameworkWorks()に変更し、テストを再実行して問題がないことを確認します。3のように、すべて緑のままとなるはずです。これで、「形」の続きに移動する準備が整いました。

Green Gilded Rose tests after the first simple refactoring

3最初の単純なリファクタリング後に緑の状態になったギルド・ローズのテスト

ここでギルド・ローズの要件を確認し、その要件をプロジェクトに組み込むことで、ドキュメントにアクセスしやすくし、その利便性を高めます。

 

手順A2-2:プロジェクトに要件を追加する

プロジェクトに要件を追加し、アクセスしやすくするには、次のようにします。

  1. ファイル・ブラウザに戻り、GildedRoseRequirements.txtというファイルを探します。このファイルをプロジェクト・ブラウザのGilded Roseにドラッグ・アンド・ドロップします。
  2. OK」をクリックして確定します。
  3. ファイルを開き、読んでみてください。

なかなかややこしい要件です。わかりにくい項目もあれば、互いに矛盾しているように見える項目もあります。これは意図的なものです。ギルド・ローズの「形」は現実世界におけるコーディングの問題をリアルにシミュレートするように設計されているからです。

スクロールして読み進めるときは、ギルド・ローズに1つの新機能を追加することが目的であることを忘れないでください。次に、コード本体を見てみます。src→main→javacom.gildedroseパッケージを開き、GildedRose.javaファイルを確認します。

ソース・コードを読んで、以下の点に注目してください。

  • updateQuality()というメソッドには、1画面に収まらないほど多くの行のコードが含まれています。とても長いメソッドなので、まさにここに「コードの臭い」があると言えます。
  • ifelsenotandが多用されています。ややこしく、読みにくいプログラム・ロジックが多数あります。
  • 50など、ハードコードされた数字が何度も繰り返されています。多数のややこしいビジネス・ロジックの存在は、技術的負債を抱えていることを暗示しています。アプリケーションを更新しにくくなっている可能性があるためです。

要件ドキュメントはかなりややこしいものでしたが、それを見て新機能を追加するのは簡単だと思われたかもしれません。しかし、このコードを見た後では、あまり確信を持てなくなっているのではないでしょうか。こういった状況は、実際の仕事でレガシーなソース・コードを扱う場合にはよく起こります。他人の書いたコードがややこしくてわからないという状況です。この「形」が現実世界におけるコーディングの問題をリアルにシミュレートしているのは、このような部分です。

レガシー・コードを扱うための「テスト-リファクタリング-追加」の手法:このレガシーなソース・コードに新機能を追加できるようにするため、以下の手法を使用します。

  1. ピンダウン・テストの追加:多数のテストを追加します。これらのテストには、2つの役割があります。1つはレガシー・コードの外部動作を理解できるようにすることです。もう1つは、コードの動作を明らかにして(ピンダウン)、何かが壊れた場合にそれがわかるようにすることです。
  2. リファクタリング:レガシー・コードをシンプルにして改善します。その際に何も壊れていないことは、ピンダウン・テストが保証してくれます。
  3. 新機能の追加:シンプルになったコードを使えば、新機能の追加ははるかに容易になります。新機能はTDDを使って作成します。

最初のピンダウン・テストを追加する前に、「形」の作成者が提供しているテスト・コードを確認しましょう。GildedRoseTestというファイルを開き、動作の仕組みに注目します。

  • 11行目で、アイテムの配列を作成している
  • 12行目で、アイテムの配列を渡してGildedRoseアプリケーションのインスタンスを作成している
  • 13行目で、updateQuality()を呼び出している
  • 14行目で、配列に含まれるアイテムの現在の状態をアサートしている

システムのピンダウン・テストを書く際は、上記と同じパターンに従います。

 

手順A2-3:最初のピンダウン・テストの追加

もう一度要件のファイルに戻り、内容を確認しましょう。最初の要件には、こう書かれています。

「すべてのアイテムに、SellIn値があります。この値は、アイテムを販売する日数を表します。すべてのアイテムに、Quality値があります。この値は、アイテムの価値を表します。1日の終わりになると、システムはすべてのアイテムについて、両方の値を減らします。」

この動作を確認するピンダウン・テストを書きましょう。まず、GildedRoseTestクラスに戻ります。16行目で[Enter]キーを押し、次に示すsystemLowersValues()メソッドを追加します。

@Test
    void systemLowersValues() {
        Item[] items = new Item[] { new Item("foo", 15, 25)};
        GildedRose app = new GildedRose(items);
        app.updateQuality();
        assertEquals(14, app.items[0].sellIn);
        assertEquals(24, app.items[0].quality);
    }

systemLowersValues()メソッドでは、sellInqualityの値が減ることをテストします。systemLowersValues(システムが値を減らす)という名前にしたのはそのためです。残りのテストも、テスト・クラスで提供されている、この最初のメソッドと同じパターンに従って記述します。

アイテムの配列を作成し、0より大きい値を割り当てます。両者を区別できるように、sellInは15、qualityは25にします。

前述のアイテムを使って、ギルド・ローズのアプリケーションのインスタンスを作成しました。updateQuality()メソッドを呼び出し、想定される動作に基づいていくつかのアサーションを行いました。

  • 最初のアサーションは、sellIn値です。sellInは1つ少なくなると考えられますが、どうなるか見てみましょう。
  • もう1つの最初のアサーションは、quality値です。quality値は1つ少なくなると考えられます。そのとおりかどうか、見てみましょう。

テストを再実行し、どうなるかを確認します。4に示すように、テストはすべて緑の状態になりました。これで、システムの動作についてあることがわかります。sellIn値が15、quality値が25のアイテムを渡し、21行目のようにしてupdateQuality()を呼び出すと、sellIn値は1つ少なくなり、quality値も1つ少なくなります。

Running the first pin-down test

4最初のピンダウン・テストの実行

 

手順A2-4:ピンダウン・テストの追加

2つ目の動作のピンダウン:要件を確認し、さらなるピンダウン・テストを追加しましょう。ドキュメントの18行目には、こう書かれています。

「販売期限が切れると、Quality2倍の速さで減ります。」

この動作を確認するピンダウン・テストを追加します。24行目で[Enter]キーを2回押し、qualityDegradesTwiceAsFast()というメソッドを追加します。今回は、すべてのコードを手入力しなくてもよいように、先ほどのテストをそのままコピーして貼り付けます。

要件には、アイテムを販売できなくなる(つまり、sellInが0になる)と、qualityは2倍の速さで減ると書かれています。したがって、sellIn値は0とし、quality値は17(0より大きい数です)などとする必要があります。期待される動作を確認すると、qualityは17から15に減り、それまでの2倍の速さで減るでしょう。最終的なメソッドは次のとおりです。

@Test
    void qualityDegradesTwiceAsFast() {
        Item[] items = new Item[] { new Item("foo", 0, 17) };
        GildedRose app = new GildedRose(items);
        app.updateQuality();
        assertEquals(15, app.items[0].quality);
    }

テストを実行すると、緑の状態になります。さらなる動作をピンダウンしました。

3つ目の動作のピンダウン:要件ドキュメントの次の項目(19行目)には、次のように書かれています。

「アイテムのQualityは負になりません。」

これを確認するピンダウン・テストを追加しましょう。32行目で[Enter]キーを2回押し、コピーと貼り付けを使ってqualityIsNeverNegative()というメソッドを追加します。qualityを0から始めて、負になるか(誤り)、0のまま(望む動作)になるかを確認します。メソッドは次のとおりです。

@Test
    void qualityIsNeverNegative() {
        Item[] items = new Item[] { new Item("foo", 0, 0) };
        GildedRose app = new GildedRose(items);
        app.updateQuality();
        assertEquals(0, app.items[0].quality);
    }

テストを実行し、動作が正しいかどうかを確認します。すべて緑の状態なので、この動作もピンダウンしました。そろそろコツをつかめてきたのではないかと思いますが、いかがでしょうか。次は、別の問題、すなわち技術的負債に取り組むことにしましょう。

 

手順A2-5:ピンダウン・テストのリファクタリング

これまでは、新しいピンダウン・テストを追加するときに、コードをコピーして貼り付けてきました。この点から、それらのテストによって新たな技術的負債が導入されたことがわかります。コピーおよび貼り付けは、DRY原則(Don’t Repeat Yourself)に反しているからです。最後のテスト・メソッドには、以下のような行が含まれています。

Item[] items = new Item[] {new Item("foo", 0, 0)};
GildedRose app = new GildedRose(items);
app.updateQuality();

これらの行は、複数のテストの中で何度も繰り返されています。app.items[0].somethingといったフレーズも同様です。これは不格好であり、エラーにつながりやすく、メンテナンスも困難です。そこで、以下のようにして簡略化しましょう。ファイルの一番上までスクロールしてください。10行目のjunitFrameworkWorks()メソッドの内部で、[Enter]キーを押します。

リファクタリングのためのコードを書く前に、リファクタリングの設計を行いましょう。ほしいのは、アイテムを返すメソッドです。このメソッド(下記参照)をcreateAndUpdate()と呼びましょう。入力は、sellInqualityの2つです。

...
Item item = createAndUpdate(0, 0);
...

もちろん、このメソッドはまだ存在しません。そのため、エディタでは赤く表示されます。ここでは、IntelliJにメソッドを作成してもらいます。返すのはアイテムです。最初の入力は整数でsellInという名前にします。2つ目の入力も整数で、名前はqualityです。最後に、12行目から14行目までの繰り返し使われている行を切り取り、そのコードを新しいメソッドに貼り付けます。結果は次のようになります。

private Item createAndUpdate(int sellIn, int quality) {
    Item[] items = new Item[] {new Item("foo", sellIn, quality)};
    GildedRose app = new GildedRose(items);
    app.updateQuality();
    return app.items[0];
}

13行目に注目してください。赤くなっている部分があります。これは実行前のコンパイラ・エラーです。app.items[0].nameitem.nameに変更します。テストを実行し、問題がないことを確認します。すべて緑の状態のままです。

調整すべき箇所が1つあります。12行目が空行になっています。空行を削除してテストを再実行します。空行を削除しただけであれば、その後にテストを再実行する必要はないように思えるかもしれません。しかし、本当に大切なのは、動きを繰り返し練習することです。そうすれば、実際のコードに触れたとき、コードを変更するたびに必ずテストを再実行することが習慣となっているでしょう。

さて、ここでさらにリファクタリングすべき点は、新しく追加したプライベート・メソッドの位置です。現在、このメソッドは2つのテストの間にあります。すべてのテストがまとまるように、このメソッドをファイルの一番上に移動しましょう。対象のメソッドを切り取り、9行目に貼り付けます。テストを再実行し、問題がないことを確認します。すべて緑の状態のままなので、作業を続行できます。

次に、残りのテスト・メソッドで同じ変更を行います。メソッドの変更が終わったらテストを再実行し、緑の状態であれば次に進みます。このプロセスが終わると、すべてのテストが緑の状態です。そしてテスト・コードは、はるかに読みやすく、メンテナンスしやすくなっています。

次の要件に関するピンダウン・テストの作成:要件ドキュメントに戻り、さらに作成すべきピンダウン・テストを確認しましょう。今度の要件は20行目です。こう書かれています。

Aged Brie(熟成したブリー・チーズ)は、古くなるほど価値が上がります。」

これまでのピンダウン・テストのパターンに従い、この動作のテストを追加します。たとえば、次のようになります。

@Test
    void agedBrieIncreasesInQuality() {
        Item item = createAndUpdate(15, 25);
        assertEquals(26, item.quality);
    }

テストを実行し、緑の状態になるかどうかを確認しましょう。残念ながら、赤の状態となります。そこで、テストを開いて詳しく調べてみます。5に示すように、qualityが増えるどころか1つ減ってしまったようです。

The test for aged Brie failed

5Aged Brieに関するテストの失敗

これは期待される動作ではありません。何がよくなかったのかわかった方もいるでしょう。種類がAged Brieのアイテムではなく、通常のアイテムを作成したのです。

 

手順A2-6:メソッドのシグネチャを変更するリファクタリング

createAndUpdate()メソッドに戻り、別のアイテムに変えられるように変更しましょう。見ておわかりのように、現在はAged Brieではなく、fooという名前のアイテムを作成しています。細かなリファクタリングを行って、これを修正します。

  1. createAndUpdate()メソッドを右クリックします。「Refactor」、「Change Signature」の順に選択します。
  2. +」をクリックすると、新しいパラメータのtypeStringに、変数名がnameになります。これがアイテムの名前になります。デフォルト値をfooにします。デフォルト値を指定すると、IntelliJは新しいパラメータを使ってすべての既存のコードを更新してくれます。[Enter]キーを押します。
  3. nameをこのメソッドの最初のパラメータにする必要があるので、nameを一番上に移動して「Refactor」をクリックします。これでリファクタリングが実行されます。

IDEによって、createAndUpdate()メソッドのシグネチャに新しいパラメータが追加され、その他のコードも更新されました。この点が、IntelliJにリファクタリングを行ってもらうことを筆者が好む理由の1つです。IntelliJでは、リファクタリングの多くで、その処理が自動化されており、コードが正しく動作するように、すべてのコードが更新されます。

次に、10行目に注目してください。この部分のコードでは、アイテムの名前としてfooが使われたままとなっています。ここでは、ハードコードされた文字列fooではなく、入力パラメータnameとする必要があります。新しく作成した、Aged Brieのテストに戻ります。アイテムの種類を示すnameパラメータの値を"Aged Brie"に変更してテストを実行し、緑の状態になるかどうかを確認しましょう。その結果、緑の状態となります。これで、レガシー・システムに要求される動作をさらにピンダウンしました。アイテムの名前が"Aged Brie"である場合、毎日価値が増加します。

この手順では、シグネチャの変更という新たな種類のリファクタリングを紹介しました。メソッドで、入力パラメータの追加または削除や、戻りタイプの変更を行う場合に使用してください。

 

手順A2-7:残りの要件に関するピンダウン・テストの作成

要件ドキュメントに基づいて、複数のピンダウン・テストをさらに追加します。このテストは、GitHubで確認できます。次に進む前に、さらなる練習として同じようにピンダウン・テストを追加してみてください。

コード・カバレッジ・ツールでその他の動作を確認する:ドキュメントの要件をすべて網羅するために多数のピンダウン・テストを作成してきましたが、本当にすべてのテストですべての要件が網羅されているのどうかわからないという方もいるかもしれません。皆さんが知らないことで、従わなければならない何かが他にあるのかもしれません。

見逃している要件を突き止めるには、コード・カバレッジ・ツールを使用します。そして、まだ実行されていないコードの外部動作を確認し、そこに到達するテストを新たに作成します。これを行うには、IntelliJのカバレッジ・ツールを使ってギルド・ローズのテストを実行します。他の開発環境にも、このツールに相当する機能が存在するはずです。

  1. IntelliJのウィンドウの右端で、6のようなアイコンをクリックします。このアイコンは、Run with code coverageを表しています。コード・カバレッジのサマリー・パネルが現れ、テストのコード・カバレッジ結果が表示されます。
  2. com.gildedroseパッケージをダブルクリックし、実際のコードのクラス2つに対する結果を確認します。7に示すように、GildedRoseクラスは93 %、Itemは83 %のコード・カバレッジです。

The code-coverage icon

6コード・カバレッジのアイコン

Code coverage panel

7コード・カバレッジ・パネル

Item.javaのクラスはまだ見ていなかったので、ここで見てみましょう。ソース・コード・ウィンドウの左側で、11行目から15行目にかけて緑色のマークが付いていることに注目してください。このマークは、テストでその行のコードが実行されたことを表します。しかし、19行目には、8のように赤いマーカーが付いています。このマーカーは、テストでその行のコードが実行されなかったことを表します。

Class code coverage information showing that line 19 has not been touched by the tests

8クラスのコード・カバレッジ情報(テストで19行目が実行されていないことを示す)

19行目はtoString()メソッドであり、デフォルトのメソッドをオーバーライドしています。ここでは、この行のコードを実行するテストを書く必要はないと判断しましょう。

GildedRose.javaソース・コードに戻ります。うれしいことに、左側に緑色のマークが多数表示されています。つまり、テストによってほとんどのコードが実行されたのです。9に示すように、まだ実行されていないのは、55行目と56行目の2行だけのようです。

GildedRose class code coverage showing lines 55 and 56 have not been touched by the tests

9GildedRoseクラスのコード・カバレッジ(テストで55行目と56行目が実行されていないことを示す)

カバレッジのサマリー・パネルを閉じ、問題のコードを調べてみます。この2行を実行するテストは、どうすれば作成することができるでしょうか。周囲を見て、このコードが43行目のif文で囲まれていることに注目してください。そこから、「種類がAged BriesellIn値が0より小さいアイテムは、価値が1つ増える」という、明文化されていない暗黙の要件があることがわかります。

 

手順A2-8:コード・カバレッジで明らかになった内容に基づくピンダウン・テスト

55行目と56行目に到達するピンダウン・テストを書きましょう。GildedRoseTestクラスに戻り、次に示す新しいテストを追加します。

@Test
    void agedBrieNeverExpires() {
        Item item = createAndUpdate("Aged Brie", 0, 42);
        assertEquals(0, item.sellIn);
        assertEquals(43, item.quality);
    }

このテストでは、名前が"Aged Brie"sellInが0(この値とすることで、55行目と56行目のコードに到達するはずです)、qualityが42の新しいアイテムを作成しています。Aged Brieの価値は常に増加するので、sellIn値は0のまま、qualityは増加するものと考えられます。

テストを実行し、どうなるかを確認しましょう。なんと、赤の状態となります。その理由は、qualityの期待値が0であったにもかかわらず、実際の値が-1であったからだと報告されています。ここで明らかなのは、実際の動作が、sellIn値が0であるAged Brieの場合でも、sellInが1つ減るようになっていることです。そこで、90行目の0を-1にします。

assertEquals(-1, item.sellIn);

ところで、これは新機能のTDDとは異なります。ここでの目的は、レガシー・コードを理解することだからです。つまり、現在行っていることを割り出すことが目的です。たとえその動作が、要件ドキュメントや、こうあるべきと皆さんが考えることと違っていたとしてもです。

そのため、このテストは厳密に言えば赤の状態です。期限切れのAged Brieの動作はエラーの可能性がありますが、その動作を変更するためにレガシー・コードを修正するのではなく、現在の動作を検証するピンダウン・テストを新たに書きます。これにより、今後、その動作が変わっているかどうかを確認できます。

テストを再実行すると、残念ながら、まだ赤の状態です。その理由は、期待される結果が43で、実際の結果が44だからです。qualityが、1つではなく2つ増えています。91行目を変更してこの動作をピンダウンし、テストを再実行します。

assertEquals(44, item.quality);

再び、すべて緑の状態になりました。つまり、このテストでAged Brieの動作がピンダウンされました。コード・カバレッジ付きで再実行し、すべてのコードの行が網羅されているかどうかを確認します。GildedRose.javaのソース・コードのファイルに戻ると、10に示すように、テストで55行目と56行目が実行されて緑色になっています。

All the code in GildedRose is now covered by tests

10テストでGildedRoseのすべてのコードが網羅された状態

 

ブランチを網羅するための別の技法:レガシー・コードの動作を確定させるピンダウン・テストはほとんど書き終えました。コード・カバレッジ・ツールを使用し、テストがレガシー・コードの100 %の行を網羅していることも確認しました。しかし、ブランチ(分岐)についてはどうでしょうか。

GildedRose.javaにある多数のif/else文をご覧ください。どう思うでしょうか。筆者の意見を述べましょう。コード・カバレッジによれば、すべて緑色にマークされています。この点は問題なく、テストでこれらの行が実行されていることになります。しかし、それで十分なのでしょうか。十分かもしれませんし、十分でないかもしれません。

trueまたはfalseのどちらかに評価できるifを考えてみましょう。ピンダウン・テストでは、両方の条件を評価しているわけではないかもしれません。そのため、それぞれのif行について、trueの場合とfalseの場合の両方を試しているかどうかはわかりません。そのため、次に必要なタスクは、ブランチ・カバレッジ・ツールでテストを実行することです。

IntelliJでブランチのカバレッジを有効化するには、次の操作を行います。

  1. 11に示すように、「Run/debug configuration」、「Edit Configurations」の順に選択します。

 

Editing the run/debug configurations in IntelliJ to enable branch coverage

11IntelliJで実行/デバッグの構成を編集し、ブランチのカバレッジを有効化

  1. 右側で「Code Coverage」タブ、「Tracing」オプションの順に選択し、「OK」をクリックします。

正しく動作することを確認するため、再度「Run with Coverage」をクリックします。GildedRose.javaに戻ります。先ほどと違う点があることに気づかれたでしょうか。緑色の行の一部が、濃い緑色(筆者の場合です。テーマの設定によっては、違う色かもしれません)になっているはずです。濃い緑色は、その行が実行されたものの、ブランチのtrueまたはfalseのどちらか片側しか実行されていないことを示しています。12をご覧ください。

GildedRose branch coverage

12GildedRoseのブランチのカバレッジ

まだやるべきことがあるのは明らかです。次のタスクは、それぞれのif文でブランチの両側が実行されるようなピンダウン・テストを追加することです。そうすれば、truefalseの両方のパスが確実に検証されます。

 

手順A2-9:ブランチのカバレッジのピンダウン

25行目から始めましょう。コードのこの行に到達するにはどうすればよいでしょうか。12行目まで戻り、意味を考えてみます。次のような意味であると思われます。アイテムの名前が”Aged Brie”または”Backstage pass”(舞台裏通行証)であり、qualityが50未満であれば、価値が増加します。アイテムの名前がBackstage passの場合、sellIn値が11未満であるかどうかを確認します。つまり、コンサートの日が近づいており、アイテムの価値が50未満であるかどうかを調べています。この条件を満たしている場合、もう一度アイテムのqualityを増やします。つまり、qualityが2つ増えることになります。

25行目に到達するには、どうすればよいでしょうか。GildedRoseTest.javaに戻ってテストを作成してみてください。たとえば、次のようなものはどうでしょうか。92行目の末尾で[Enter]キーを2回押し、次に示す新しいテスト・ケースを追加します。

@Test
void backstagePassMaximumQuality() {
  Item item = createAndUpdate("Backstage passes to a TAFKAL80ETC concert", 10, 48);
  assertEquals(50, item.quality);
}

このテスト・ケースは、Backstage passとその価値の最大値に関するものです。要件ドキュメントには、価値の最大値は50であると書かれています。上記のコードでは、sellIn値を10(11より小さい数です)、quality値を48(50より小さい数です)としてBackstage passというアイテムを作成しました。このテストでは、価値が2つ増加して48から50になるはずです。テストを実行し、動作を本当に正しく理解しているかどうかを確認します。テストは緑色の状態なので、うまくいきました。

しかし、もう1つ残っています。iftruefalseの両方を実行するには、ブランチのもう片方で、qualityが49のBackstage passと、価値が50を超えないことを確認するテストが必要です。96行目と97行目の2行をコピーし、99行目に貼り付けます。次に、価値が49で始まる別のテストを作成します。たとえば、次のようになります。

item = createAndUpdate("Backstage passes to a TAFKAL80ETC concert", 10, 49);
assertEquals(50, item.quality);

テストを再実行しましょう。期待される動作を理解していたようです。それでは、ブランチのカバレッジ付きでテストを実行し、GildedRose.javaの25行目にあるブランチの両側が実行されているかどうかを確認します。13に示すように、行が緑色にマークされています。

The test successfully pinned down both branches at line 25

1325行目のブランチの両側がピンダウンされた状態

次の手順では、残りのピンダウン・テストを追加し、ブランチのカバレッジ100 %を達成します。

 

手順A2-10:ブランチの完全カバレッジを実現するための仕上げのピンダウン・テスト

100 %のカバレッジ(14参照)に到達するピンダウン・テストをコード・リポジトリに追加してあります。詰まってしまった方は、自分のテストと比較してみてください。

Full branch coverage achieved in the pin-down tests

14ピンダウン・テストで実現した、ブランチの完全カバレッジ

 

まとめ

技術的負債の多いレガシー・コードの動作を確定させる手順や技法について、多くのことを学びました。これにより、不注意なプログラミングによって紛れ込んだ効率の悪さやエラーに対処する準備ができたことになります。

最初に学んだのは、レガシー・コードのピンダウン・テストを作成し、その動作を理解する方法です。多数の技法を使って要件ドキュメントを網羅しました。コード・カバレッジ・ツールを使い、テスト・ケースで実行されていない部分があるかどうかも確認しました。そして最後には、ブランチ・カバレッジ・ツールを使ってコードのすべてのif/elseのブランチを確認し、テストでコードが100 %網羅されるようにしました。

これで、レガシー・コードのリファクタリングを開始し、動作を改善する自信がついたはずです。実際にそれを行うのは、最後の記事となるパート3になります。リファクタリングを使用して、レガシー・コードをシンプルにし、重複を削除して、再利用可能なオブジェクトを増やす予定です。最後に、シンプルになったレガシー・コードベースにすばやく新機能を追加することを通して、リファクタリングによってアジャイル・ワークフローが補完されることを確認します。

 

さらに詳しく

 

Mohamed Taman

 

Mohamed Taman(@_tamanm):SiriusXI InnovationsのCEOであり、Effortel Telecommunicationsの最高ソリューション・アーキテクト。セルビアのベオグラードを拠点とするJava Champion、Oracle Groundbreaker、JCPメンバーであり、Jakarta EEのAdopt-a-SpecプログラムとOpenJDKのAdopt-a-JSRのメンバーを務める。