私はしばし、ci-phpunit-testを使ったテストの書き方で、CodeIgniter3自体の挙動を説明しなければならないことがあります。
私は、その説明が非常にめんどくさいと感じているのでここに記載します。

知っておいて欲しい実装は次の4つです。

  • Controllerの実装(設計) <- イマココ
  • Controllerの実装(コンストラクタ)
  • 依存性解決の実装
  • Modelの実装

Controllerの実装(設計)

ControllerはSingletonのような神クラスです。

Sigletonとは

Sigletonとは、そのクラスのインスタンスが一つしか保証されないクラスです。
おそらく実装を見た方が理解が早いと思います。

次のようなクラスが存在する場合は、getInstanceメソッドを経由してしかSingletonクラスをインスタンス化できません。
また、newやcloneは利用できず、PHPを実行するプロセスの中でインスタンスが一つしか生成することができません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton
{
private static $instance;

private function __construct() {}

public function __clone() {
throw new LogicException('This is Singleton');
}

public static function getInstance()
{
if (!self::$instance instanceof Singleton) {
self::$instance = new Singleton();
}

return self::$instance;
}
}
1
2
3
4
5
6
7
8
// コンストラクタがprivateなのでエラーが発生します
$obj = new Singleton(); // fail

// getInstanceを経由してSingletonインスタンスを取得することができます。
$singleton = Singleton::getInstance(); // success

// cloneすると例外が発生します。
$new_singleton = clone $singleton; // fail

Controllerは神クラス

CodeIgntierのControllerは上記で説明したSingletonに似た神クラスです。
Singletonと違い、cloneやnewを行うことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// コンストラクタがpublicなのでエラーが発生しません。
$ci_controller = new CI_Controller(); // success

// 1度でもnewしていると、get_instanceからCI_Controllerインスタンスを取得できます。
$ci_controller2 = CI_Controller::get_instance(); // success

assert($ci_controller === $ci_controller2); // success

// cloneすることが可能です。
$new_ci_controller = clone $ci_controller; // success

// cloneしているのでもちろん別のインスタンスです。
assert($ci_controller === $new_ci_controller); // fail

// 別のインスタンスを新たに作ることももちろんできますが、インスタンスは異なります。
$second_ci_controller = new CI_Controller(); // success

assert($ci_controller === $second_ci_controller) // fail

つまり、CI_Controllerは一意でない可能性はあり得る構造になっています。
しかし、システム内では基本的にget_instance関数を用いてSingletonの様に扱っています。

Singletonの様に扱うことで現在ユーザからアクセスされているControllerと同一のインスタンスを利用しやすい設計になっています。

ここまでのまとめ

  • システム内でget_instance関数を用いて、Controllerの同一なインスタンスを利用しやすい様になっています。
  • Controllerインスタンスは複数のインスタンスを生成することができます。

テスト時に知っておきたいこと

実際にテストコードに落とし込んで知っておきたい事を記します。
次の例は、Controllerのテストコードです。

テスト対象のController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class Test_target extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load->model('todo_model');
}

public function list()
{
$list = $this->todo_model->get_list();

$this->output->enable_profiler(false)
->set_content_type('application/json; charset=UTF-8')
->set_output(json_encode(
$list,
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
));
}
}

Controllerの同一なインスタンスを利用しやすい様になっている

ci-phpunit-testでは実際にCodeIgniterで作成したアプリケーションへリクエストを行なった時と同じ様に振る舞うrequestメソッドがあります。

requestメソッドを行う際に、テストコード実装者が意図した振る舞いをするMockオブジェクトに差し替えたいと考えるでしょう。

そんな時は、Controllerのメソッドが呼び出される前処理を挟むsetCallableを利用すると良いです。

setCallableでは引数に渡した関数の第一引数としてControllerインスタンスが渡されます。
このControllerインスタンスは、テスト対象のリクエストされるControllerインスタンスと同一です。

第一引数へ渡された、ControllerインスタンスのプロパティにMockオブジェクトを注入することができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class Test_target_test extends TestCase
{
public function test_setCallable_argument_CI_equal_target_controller()
{
// $CIとは、requestを実行する際のControllerインスタンス(Test_target)である。
$this->request->setCallable(function ($CI) {
$CI->todo_model = $this->getDouble(Todo_model::class, [
'get_list' => ['sample' => '1']
]);
});

$output = $this->request('test_target/list');
$this->assertContains('"sample"', $output);
}
}

Controllerインスタンスは複数のインスタンスを生成することができる

先ほどとは打って変わって、次のコードではテスト対象のController(Target_test)とは異なるControllerインスタンスへMockオブジェクトを注入してしまっている例です。
このテストではMockオブジェクトがテスト対象のController(Target_test)からアクセスされることはありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class Test_target_test extends TestCase
{
public function setUp()
{
$this->resetInstance();
$this->CI->todo_model = $this->getDouble(Todo_model::class, [
'get_list' => ["sample" => 'this is unit test']
]);
}

public function test_target_controller_not_equal_this_CI()
{
// $this->CIとTest_targetコントローラは違うインスタンスである
$output = $this->request('test_target/list');
$this->assertContains('"sample"', $output);
}
}

これは、Test_targetコントローラのインスタンスと、$this->CIで持っているControllerインスタンスが異なるためです。