Symfony meetup #13
LT 発表
レガシーコードのテストを書いていくテクニック
はじめに
- レガシーコードにテストを書くのはきつい。
- 頑張ってE2E書くか、fixtureを用意してDBを使ったテストをする (むしろそれしか方法がないぐらい)
- でもクリティカルなところはユニットテストで確かめながら開発したい
じゃあ、ユニットテストどうするか?
レガシーコード改善ガイドを参考にする
- スプラウトメソッド
- 新しく機能を追加する場合には新しくメソッドを作る。そこにテストを書く
- ラップメソッド
- 新しく機能を追加する場合に既存のメソッドに手を入れず、ラップしたメソッドに機能を追加する
新しく書くコードなら テストできる \(^o^)/
スプラウトメソッドのサンプル
機能追加前
(レガシーコードはだいたいstatic…)
// legacyなstaticメソッドはテストが困難
public static function legacyFunction()
{
// very long legacy code ...
}
機能追加後のコード
class LegacyClass
{
public static function legacyFunction()
{
// very long legacy code ...
if ($someCondition) {
return self::newFeature();
}
// very long legacy code ...
}
protected static function newFeature() // ここはテストコードでオーバーライドするため protected にしてある
{
// 新しく足したメソッドはテストコードでカバーする
}
}
class LegacyClassForTest extends LegacyClass
{
// overrideして強引にアクセス権をpublicにする
public static function newFeature()
{
return parent::newFeature();
}
}
class LegacyClassTest extends \PHPUnit_Framework_TestCase
{
public function testNewFeature()
{
// publicに変えたメソッドに対してテストする
$this->assertSame($expected, LegacyClassForTest::newFeature());
}
}
解説
LegacyClassForTest
というのを作り無理矢理アクセス権をpublicに変更しています。
このように、対象クラスのテスト用サブクラスを作成する方法はテスト技法として時折採用されていて、 OSSのテストコードなどでもたまに見受けられます。
「Test-Specific Subclass」と呼ぶそうです。
アクセス権を public に変更するだけであればReflection
を使っても同等のことは実現できますが PHPStorm などで静的にコードを解析できるメリットを考えるとこちらの方がいいのではないかと考えています。
ラップメソッド
class LegacyClass
{
public function newFeature()
{
if ($someCondition) {
return $someValue;
}
return static::legacyFunction(); // テストコードでoverrideするため self:: ではなく static:: を使う
}
public static function legacyFunction()
{
// very long legacy code ...
}
}
テストコードはスプラウトメソッドとほぼ同じになるので省略
まとめ1
- 新しく書くコードにはテストを書こう!
- サブクラスでオーバライドしてstub化することで依存を排除することができる
- 依存がなければテストをかけるようになる
テストコード例
環境に依存してテストしにくいものをテストする
例) 日付に依存したコードのテストの仕方
class LegacyClass
{
const START_DATE = '2016-01-01 00:00:00';
const END_DATE = '2016-01-31 23:59:59';
public static function legacyFunction()
{
if (self::isCampaignTerm()) {
return self::newFeature();
}
// very long legacy code ...
}
protected static function isCampaignTerm()
{
// ここはオーバライドしたいので "self::" ではなく "static::"
$today = static::getToday();
return new DateTime(self::START_DATE) <= $today && $today <= new DateTime(self::END_DATE);
}
protected static function getToday()
{
return new DateTime('today');
}
}
class LegacyClassForTest extends LegacyClass
{
private static $today;
public function setTodayForStub(DateTime $today)
{
self::$today = $today;
}
// Stub化して値を返す
protected static function getToday()
{
return self::$today;
}
}
class SomeClassTest extends \PHPUnit_Framework_TestCase
{
public function testLegacyFunction()
{
// 日付Stub化して返す
LegacyClassForTest::setTodayForStub(new DateTime('2016-01-01');
$this->assertSame($expected, LegacyClassForTest::legacyFunction());
}
}
解説:
-
new DateTime('today')
というインスタンス化するコードを別メソッドに切り出す - 切り出したメソッドをオーバーライドすることで、Stubなりモックなりに差し替えることができる
無理矢理感はありますが、テストしないよりはましなはず。。
DIできる環境なら、Clockクラスを差し込みたい。。
【参考】
http://phpmentors.jp/post/46982737824/時計オブジェクトドメインクロックを導入してテスト容易性と意図性を高める
その他、DBや外部システムなどに依存したメソッドである場合などにも同様にテストを書くことができると思います。
PHPのビルトイン関数に依存したコードのテスト
その名前空間に対して同名の関数を定義する
【参考】
BEAR.Sundayから学ぶテストプラクティス
http://qiita.com/okapon_pon/items/21616e14e8d4d8f1c2bc
もちろん、PHPのビルトイン関数を直接使わず、ラッパークラスを用意するほうが望ましいです。
まとめ
- 日付・時刻への依存を排除してコードを書く
- PHPのビルトイン関数に依存した関数もその名前空間に対して同名の関数を定義することで(Stub化することができる)
- 基本的にはラッパークラス作る方がいいです。