Help us understand the problem. What is going on with this article?

レガシーコードのテストを書いていくテクニック

More than 1 year has passed since last update.

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化することができる)
    • 基本的にはラッパークラス作る方がいいです。
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away