45
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-10

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化することができる)
    • 基本的にはラッパークラス作る方がいいです。
45
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?