テスト不能な PHP コードをリファクタリングするための戦略

レガシー PHP コードのユニット・テストとリファクタリングを行うことで、テストを容易にし、コード品質を改善する

PHP が単純なスクリプト言語から本格的なプログラミング言語へと成長するのに伴い、典型的な PHP アプリケーションのコード・ベースも次第に複雑なものになってきました。これらのアプリケーションのサポートおよび保守を管理するには、そのプロセスを自動化する上で役立つさまざまなテスト・ツールを使用することができます。1 つの方法として、ユニット・テストを使用すると、作成されたコードが正しいかどうかを直接テストすることができます。しかし、レガシー・コード・ベースの場合は、この種のテストを適用できないことが多々あります。この記事では、よくありがちな問題を抱える PHP コードをリファクタリングすることで、一般的なユニット・テスト・ツールを使って容易にテストを行えるようにする一方、依存関係をなくしてコード・ベースのクオリティーを向上させる、という戦略について説明します。

John Mertic, Software Engineer, SugarCRM

John Mertic は SugarCRM のソフトウェア・エンジニアであり、PHP による Web アプリケーションを数年間経験してきています。彼はこれまで SugarCRM で、データ統合や、モバイル・インターフェースやユーザー・インターフェースのアーキテクチャーなどを専門としてきました。彼は精力的なライターとして、php|architect や IBM developerWorks、Apple Developer Connector などに記事を発表しており、また『The Definitive Guide to SugarCRM: Better Business Applications』の著者でもあります。彼は多くのオープンソース・プロジェクトにも貢献しており、そのうち最も重要なものは PHP プロジェクトです。彼は PHP の Windows インストーラーの作成者でありメンテナーでもあります。



2011年 7月 08日

はじめに

PHP が 15 年前に登場した頃を振り返ってみると、当初の PHP は当時よく使われていた CGI スクリプトに代わる単純で動的なスクリプト言語でした。そこから PHP は現在のような本格的なプログラミング言語へと成長を遂げました。この成長に伴い、PHP で作成したアプリケーションのコード・ベースが大きくなってくると、そのコードを手動でテストするのは不可能になってきました。コードが変更されると、その変更が大規模であれ小規模であれ、アプリケーション全体に影響を及ぼす可能性があります。その影響は、ページをロードできない、あるいはフォームを保存できない、といった単純な問題として現れることもあれば、検出するのが難しい問題や、特定の状況でのみ発生する問題となって現れることもあります。場合によると、以前にアプリケーションで発生していた問題が再び発生する場合もあります。そこで、こうしたさまざまな問題を解決するために、さまざまなテスト・ツールが開発されてきました。

よく使われるテストの方法としては、機能テストまたは受け入れテストと呼ばれる方法があります。この方法では、ユーザーとアプリケーションとの典型的なやりとりを通じてアプリケーションをテストします。この方法はアプリケーションのさまざまなプロセスをテストするのには適していますが、非常に時間がかかる可能性があり、下位レベルのクラスや関数が想定どおり動作しているかどうかを確認するテストとしては、あまり効果的ではありません。そこで、ユニット・テストという別のテスト方法が登場します。ユニット・テストの目標は、アプリケーションの基礎にあるコードの機能をテストし、そのコードを実行した場合に適切な結果が得られるかどうかを確認することです。多くの場合、「成長を遂げた」 Web アプリケーションには、時間の経過とともにテストが難しくなる可能性のあるレガシー・コードが大量に含まれているため、開発チームはアプリケーションのテスト・カバレッジとして満足のいく結果を提供できなくなります。こうしたコードはよく、「テスト不能コード」と呼ばれます。では、アプリケーションの中にあるテスト不能コードを特定する方法と、そのコードを修正する方法について見て行きましょう。


テスト不能コードを特定する

コード・ベースの中にテスト不能な問題領域がある場合、その問題領域はコードの作成時点では明確でなかった、という場合が普通です。PHP アプリケーションのコードを作成する場合、Web リクエストのフローに合わせてコードを調整しがちであるため、アプリケーションの設計に手続き型の手法がとられる傾向があります。大急ぎでプロジェクトを完成させる、あるいはアプリケーションを修正する必要があると、開発者は「手間を省き」、コードを手早く完成させてしまう場合があります。以前に作成された不適切なコードやわかりにくいコードによって、アプリケーションがテストできなくなる場合があります。開発者は、たとえ後でサポートの問題が発生するとしても、可能な限りリスクの少ない修正を試みがちだからです。これらの問題領域はどれも、大がかりな手段をとらない限り、ユニット・テスト・ツールでのテストは不可能です。


グローバルな状態に依存する関数

PHP アプリケーションではグローバル変数は便利な手段です。グローバル変数を使用することで、変数やオブジェクトをアプリケーションの最初の方で初期化することができ、その後はアプリケーションの任意の場所で使用することができます。ただし、その柔軟性には犠牲を伴います。テスト不能コードに見られる一般的な問題として、グローバル変数が多用されているのです。それを示したものがリスト 1 です。

リスト 1. グローバルな状態に依存する関数
<?php 
function formatNumber($number) 
{ 
    global $decimal_precision, $decimal_separator, $thousands_separator; 
     
    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 
     
    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator); 
}

これらのグローバル変数を使用する場合、注意しなければならない重要なポイントが 2 つあります。1 つ目のポイントは、テストの際には、それぞれのグローバル変数の値が、関数が想定している有効な値に設定されるように考慮する必要があることです。そして 2 つ目は、さらに重要なポイントとして、テストを実行した後、グローバルな状態が変更されたままで次のテストが行われてしまうことで、次のテストの結果が無効になることがないように、テストの後にはグローバルな状態をテスト実行前の状態にリセットする必要があります。PHPUnit には、グローバル変数をバックアップしてテストの実行後にリストアする機能があり、この機能を利用すれば、これらのポイントに容易に対処することができます。しかし、より望ましい手法としては、これらのグローバル変数の値をテスター・クラスによって直接渡せるような手段を用意し、その値をメソッドが使用できるようにします。リスト 2 はその方法の例を示しています。

リスト 2. 上記の関数を修正し、グローバル変数をオーバーライドできるようにする
<?php 
function formatNumber($number, $decimal_precision = null, $decimal_separator = null, 
$thousands_separator = null) 
{ 
    if ( is_null($decimal_precision) ) global $decimal_precision; 
    if ( is_null($decimal_separator) ) global $decimal_separator; 
    if ( is_null($thousands_separator) ) global $thousands_separator; 
     
    if ( !isset($decimal_precision) ) $decimal_precision = 2; 
    if ( !isset($decimal_separator) ) $decimal_separator = '.'; 
    if ( !isset($thousands_separator) ) $thousands_separator = ','; 
     
    return number_format($number, $decimal_precision, $decimal_separator, 
$thousands_separator);
}

このようにすると、コードのテストが容易になるだけではなく、コードはメソッドのグローバル変数に依存しなくなります。この方法を利用すると、このコードの残り部分をリファクタリングし、グローバル変数をまったく使用しないようにできる可能性があります。


リセットできないシングルトン

シングルトンはアプリケーションの中で 1 度に 1 つのインスタンスのみが存在するように設計されたクラスです。シングルトンはアプリケーションのグローバル・オブジェクト (データベース接続や構成設定など) に使用される共通パターンです。シングルトンをアプリケーションの中で使用するのはタブーとされることがよくありますが、多くの開発者はそのことをあまり重視するには値しないものと考えがちです。いつでも利用できるオブジェクトの方が便利だからです。その背景にはシングルトンの使いすぎがあり、いわゆる「神オブジェクト」の多くは拡張することができません。しかしテストの観点から見た大きな問題は、シングルトンが多くの場合、不変であることです。その例としてリスト 3 を見てみましょう。

リスト 3. テストの対象となるシングルトン・オブジェクト
<?php 
class Singleton 
{ 
    private static $instance; 
     
    protected function __construct() { } 
    private final function __clone() {} 
     
     
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 
         
        return self::$instance; 
    } 
}

これを見るとわかるように、シングルトンが最初にインスタンス化されると、getInstance() メソッドを何度呼び出しても同じオブジェクトが返され、新しいオブジェクトは返されません。これはこのオブジェクトを変更しようとする場合に大きな問題になる可能性があります。この問題に対する最も容易なソリューションは、このオブジェクトをリセットできるメソッドをこのオブジェクトに追加します。リスト 4 はその例を示しています。

リスト 4. シングルトン・オブジェクトにリセット・メソッドを追加する
<?php 
class Singleton 
{ 
    private static $instance; 
     
    protected function __construct() { } 
    private final function __clone() {} 
     
     
    public static function getInstance() 
    { 
        if ( !isset(self::$instance) ) { 
            self::$instance = new Singleton; 
        } 
         
        return self::$instance; 
    } 
     
    public static function reset() 
    { 
        self::$instance = null; 
    } 
}

これで、リセット・メソッドを呼び出してから各テストの実行を開始できるようになるため、すべてのテストの実行時に必ずシングルトン・オブジェクトの初期化コードを通るようにすることができます。このリセット・メソッドをアプリケーション内で使えるようにしておくと、シングルトンが容易に可変となるため、概して有用です。


クラスのコンストラクターのコードをリファクタリングする

ユニット・テストを行う際の適切なプラクティスとして、対象となる機能のみをテストし、絶対に必要なオブジェクトや変数以外は設定しないようにします。オブジェクトや変数を設定すると、後で削除しなければなりませんが、これが問題となるのは、より面倒なもの (つまりファイルやデータベース・テーブルなど) を設定した場合です。それらの状態を変更する必要がある場合には、テストの完了後、十分注意して状態を元に戻す必要があります。そのルールを厳守する上で最大の障害は、テストとは無関係のあらゆる処理を行う、オブジェクトのコンストラクターそのものです。その例としてリスト 5 を検討してみましょう。

リスト 5. 大規模なシングルトン・メソッドを持つクラス
<?php 
class MyClass 
{ 
    protected $results; 
     
    public function __construct() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable'); 
    } 
     
    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

上記の場合、このオブジェクトの fdfdfd メソッドをテストするためには、データベース接続を設定し、テーブルにレコードを用意し、テストの後にこれらのリソースをすべてクリーンアップする必要があります。しかし、これらの作業をまったく行わなくても fdfdfd メソッドをテストすることができるため、いずれの作業も過剰なように見えます。そこで、このコンストラクターをリスト 6 のように変更してみます。

リスト 6. クラスを変更し、すべての不要な初期化ロジックをスキップできるようにする
<?php 
class MyClass 
{ 
    protected $results; 
     
    public function __construct($init = true) 
    { 
        if ( $init ) $this->init(); 
    } 
     
    public function init() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $this->results = $dbconn->query('select name from mytable');
    } 
     
    public function getFirstResult() 
    { 
        return $this->results[0]; 
    } 
}

コンストラクターの大量のコードをリファクタリングし、init() メソッドの中に入れました。このようにしても、init() メソッドはデフォルトで変更前と同じようにコンストラクターの中で呼び出されるため、既存のコードに影響することはありません。一方、今度はテストの際にブール値 false をコンストラクターに渡すだけでよく、init() メソッドやすべての不要な初期化ロジックを呼び出す必要はありません。このようにクラスをリファクタリングすることで、コードも改善され、オブジェクトを作成するコードから初期化コードを分離することもできました。


クラスの依存関係がハードコーディングされたコード

この前のセクションで見たとおり、クラスの設計上の問題としてテストを困難にしている大きな問題は、テストに不要なオブジェクトを含むあらゆる種類のオブジェクトを初期化しなければならないことです。先ほどは、重たい初期化ロジックによって、テストの作成作業にあらゆる類のオーバーヘッドが追加され得ることを (特に、テストを正常に行う上でこの初期化ロジックがまったく不要な場合について) 説明しましたが、テスト対象のクラスのメソッド内で新しいオブジェクトを直接作成すると、別の問題が発生する可能性があります。そうした問題のあるコードの例として、リスト 7 を見てください。

リスト 7. 別のオブジェクトを直接初期化するメソッドを持つクラス
<?php 
class MyUserClass 
{ 
    public function getUserList() 
    { 
        $dbconn = new DatabaseConnection('localhost','user','password'); 
        $results = $dbconn->query('select name from user'); 
         
        sort($results); 
         
        return $results; 
    } 
}

例えば、私達が上記の getUserList メソッドをテストしており、テストの焦点は返されるユーザー・リストがアルファベット順に適切にソートされているかどうかの確認にあるとします。この場合、データベースからレコードを取得できるという事実は実際には重要ではありません。というのも、テストしているのはデータベースから返されるレコードをソートできるかどうかだからです。問題は、メソッドの中でデータベース接続オブジェクトを直接インスタンス化しているため、レコードの取得などの前提作業をすべて行わない限り、このメソッドを適切にテストできない点にあります。そこで、オブジェクトを途中で挿入できるように変更しましょう (リスト 8)。

リスト 8. 別のオブジェクトを直接初期化するメソッドを持つと同時に、そのメソッドを無効にする手段も持つクラス
<?php 
class MyUserClass 
{ 
    public function getUserList($dbconn = null) 
    { 
        if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) { 
            $dbconn = new DatabaseConnection('localhost','user','password'); 
        } 
        $results = $dbconn->query('select name from user'); 
         
        sort($results); 
         
        return $results; 
    } 
}

このようにすると、想定されるデータベース接続オブジェクトと互換性のあるオブジェクトを直接渡すことができるようになり、このクラスは新しいオブジェクトを作成する代わりに、渡されるオブジェクトを使用するようになります。渡されるオブジェクトは単なるモック・オブジェクトでも構いません。つまり、呼び出されるメソッドの戻り値をいくつかモック・オブジェクトの中にハードコーディングし、テストに使用するデータを直接提供することができます。この場合はデータベース接続オブジェクトのクエリー・メソッドのモックを作成し、データベースを呼び出して値を取得する代わりに単純に結果を返すことができます。このようにリファクタリングすることで、このメソッドも改善され、アプリケーションでは指定されたデフォルトのデータベース接続以外にも、さまざまなデータベース接続を使用できるようになります。


テストが容易なコードによるメリット

確かに、テストが容易なコードを作成すると、PHP アプリケーションのユニット・テストが作成しやすくなるという明らかなメリットがありますが (この記事で紹介した例でそれを見てきました)、それ以外にもそうしたコードを作成するプロセスによって、適切に設計されていてモジュール化が進んでいる上に安定しているアプリケーションを作成できるようになります。私達は皆、さまざまなレベルの「スパゲティー」コードを見てきています。それらのコードは、ビジネス・ロジックと表示ロジックが混在し、緊密に一体化された 1 つの巨大な手続き型ブロックとして PHP アプリケーションを構成しています。そうしたアプリケーションは、誰が手を付けたとしても徹底的な調査が必要なサポートの悪夢が必ず発生します。この記事では、コードのテストを容易にするプロセスの中で、問題を起こしがちなコード (設計に問題があるだけではなく、機能にも問題があるコード) をリファクタリングしました。また、依存関係のハードコーディングをなくすことにより、関数やクラスの目的を 1 つに限定せずにアプリケーションの他の領域でも使いやすくすることで、コードを再利用しやすくする方法を示しました。さらに、将来コード・ベースのサポート作業が容易になるよう、品質が悪いコードを削除し、はるかに品質の良いコードで置き換えました。


まとめ

この記事では、PHP コードのテストを容易にする方法を、PHP アプリケーションに昔から見られるテスト不能なコードの例をいくつか示しながら説明しました。また、アプリケーションの中でどのように問題が発生するかを探り、問題を起こしがちなコードを修正してテストを可能にする最善の方法を示しました。さらに、こうした変更をコードに加えることで、コードのテストが容易になるだけではなく、リファクタリングされたコード部分では全体的にコードの品質が改善され、コードの再利用もしやすくなることについても説明しました。


ダウンロード

内容ファイル名サイズ
Article source codecode_examples.zip3KB

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次のオープンソース開発プロジェクトを IBM ソフトウェアの試用版を使用して革新してください。ダウンロード、あるいは DVD で入手することができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。プロフィールで選択した情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, XML
ArticleID=696828
ArticleTitle=テスト不能な PHP コードをリファクタリングするための戦略
publish-date=07082011