LoginSignup
1
3

More than 3 years have passed since last update.

phpunitでprivateメソッドのテストを簡単に記述する

Posted at

結論

ReflectionWrapper クラスを通すことで,可視性を気にすることなくメソッド/プロパティへアクセスできる.

$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';

ReflectionWrapper のコードはこちら

https://github.com/ueba1110/utilities/tree/master/php/tests

解説

Reflectionクラスは記述が面倒

private/protected なメソッドをテストする場合, ReflectionClass で可視性を変更してアクセスする.

$testClass = new TestClass();
$reflection = new \ReflectionClass(get_class($testClass));

// private/protected なメソッドを実行
$method = $reflection->getMethod('doPrivate');
$method->setAccessible(true);
$method->invoke($testClass);

// private/protected なプロパティへアクセス
$property = $reflection->getProperty('privateProperty');
$property->setAccessible(true);
$property->setValue($testClass, 'private');

ただ ReflectionClass による記述はテストに無関係であり,見通しが悪くなる.
できれば下記程度のコード量で記述したい.

$testClass = new TestClass();

// private/protected なメソッドを実行
$testClass->doPrivate();

// private/protected なプロパティへアクセス
$testClass->privateProperty = 'private';

ラッパークラスを作って解決する

ReflectionClass に関する処理をラッパークラスに集約することで,実際のテストコードでは可視性を気にすることなくアクセスできるようにする.

<?php

declare(strict_types=1);

namespace Tests;

use ReflectionClass;

final class ReflectionWrapper
{
    /**
     * test対象クラスのインスタンス.
     *
     * @var mixed
     */
    private $instance;

    /**
     * test対象クラスのリフレクション.
     *
     * @var ReflectionClass
     */
    private ReflectionClass $reflection;

    /**
     * __construct.
     *
     * @param mixed $instance
     */
    public function __construct($instance)
    {
        $this->instance   = $instance;
        $this->reflection = new ReflectionClass($instance);
    }

    /**
     * __set.
     *
     * @param string $name
     * @param mixed  $value
     */
    public function __set(string $name, $value): void
    {
        $property = $this->reflection->getProperty($name);
        $property->setAccessible(true);
        $property->setValue($this->instance, $value);
    }

    /**
     * __get.
     *
     * @param string $name
     * @return mixed
     */
    public function __get(string $name)
    {
        $property = $this->reflection->getProperty($name);
        $property->setAccessible(true);

        return $property->getValue($this->instance);
    }

    /**
     * __call.
     *
     * @param string $name
     * @param array  $arguments
     * @return mixed
     */
    public function __call(string $name, array $arguments)
    {
        $method = $this->reflection->getMethod($name);
        $method->setAccessible(true);

        return $method->invokeArgs($this->instance, $arguments);
    }
}

上記 ReflectionWrapper を利用した場合のテストコードがこちら

$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';

ポイントはマジックメソッドの挙動である.
__set , __get , __call は,アクセス不能あるいは未定義のメソッド/プロパティへアクセスされた場合の挙動を, php標準から変更することが可能である.
ReflectionWrapper に対してメソッド呼び出しやプロパティアクセスを行うと,それらはいずれも未定義のため,各マジックメソッドが呼び出される.
各マジックメソッドでは,実際のテスト対象クラスのメソッド/プロパティを,可視性を変更した上で呼び出している.
結果,利用側ではまるで可視性を無視したような形でコードを記述できるようになる.

$testClass = new ReflectionWrapper(
    // テスト対象クラスのインスタンスを渡す
    new TestClass()
);

// private/protected なメソッドがそのまま呼び出せる
$testClass->doPrivate();

// $testClass(ReflectionWrapper) は doPrivate メソッドが定義されていない
// => __call が実行される
// => __call 内で TestClass の doPrivate メソッドが,可視性を変更した上で実行される


// private/protected なプロパティへもアクセス可能
$testClass->privateProperty = 'private';

// $testClass(ReflectionWrapper) は privateProperty プロパティが定義されていない
// => __set が実行される
// => __set 内で TestClass の privateProperty プロパティに,可視性を変更した上で代入が行われる
1
3
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
1
3