Completed
Pull Request — master (#147)
by Kenji
03:26 queued 43s
created

CIPHPUnitTestCase   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 376
rs 8.3999
c 0
b 0
f 0
wmc 38
lcom 4
cbo 4

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setCI() 0 4 1
A __get() 0 10 2
A setUpBeforeClass() 0 12 1
A resetInstance() 0 6 1
A newController() 0 7 1
C tearDown() 0 34 7
A request() 0 4 1
A ajaxRequest() 0 5 1
A getDouble() 0 4 1
A verifyInvokedMultipleTimes() 0 6 1
A verifyInvoked() 0 4 1
A verifyInvokedOnce() 0 4 1
A verifyNeverInvoked() 0 4 1
A warningOff() 0 6 1
A warningOn() 0 4 1
A assertResponseCode() 0 11 1
A assertResponseHeader() 0 16 2
C assertResponseCookie() 0 44 8
B assertRedirect() 0 37 5
1
<?php
2
/**
3
 * Part of ci-phpunit-test
4
 *
5
 * @author     Kenji Suzuki <https://github.com/kenjis>
6
 * @license    MIT License
7
 * @copyright  2015 Kenji Suzuki
8
 * @link       https://github.com/kenjis/ci-phpunit-test
9
 */
10
11
/**
12
 * @property CIPHPUnitTestRequest    $request
13
 * @property CIPHPUnitTestDouble     $double
14
 * @property CIPHPUnitTestReflection $reflection
15
 */
16
class CIPHPUnitTestCase extends PHPUnit_Framework_TestCase
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
17
{
18
	protected $_error_reporting = -1;
19
20
	/**
21
	 * If you have a route with closure, PHPUnit can't serialize global variables.
22
	 * You would see `Exception: Serialization of 'Closure' is not allowed`.
23
	 *
24
	 * @var array
25
	 */
26
	protected $backupGlobalsBlacklist = ['RTR'];
27
28
	/**
29
	 * @var CI_Controller CodeIgniter instance
30
	 */
31
	protected $CI;
32
	
33
	protected $class_map = [
34
		'request'    => 'CIPHPUnitTestRequest',
35
		'double'     => 'CIPHPUnitTestDouble',
36
		'reflection' => 'CIPHPUnitTestReflection',
37
	];
38
39
	public function setCI(CI_Controller $CI)
40
	{
41
		$this->CI = $CI;
42
	}
43
44
	public function __get($name)
45
	{
46
		if (isset($this->class_map[$name]))
47
		{
48
			$this->$name = new $this->class_map[$name]($this);
49
			return $this->$name;
50
		}
51
52
		throw new LogicException('No such property: ' . $name);
53
	}
54
55
	public static function setUpBeforeClass()
0 ignored issues
show
Coding Style introduced by
setUpBeforeClass uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
56
	{
57
		// Fix CLI args, because you may set invalid URI characters
58
		// For example, when you run tests on NetBeans
59
		$_SERVER['argv'] = [
60
			'index.php',
61
		];
62
		$_SERVER['argc'] = 1;
63
		
64
		// Reset current directroy
65
		chdir(FCPATH);
66
	}
67
68
	/**
69
	 * Reset CodeIgniter instance and assign new CodeIgniter instance as $this->CI
70
	 */
71
	public function resetInstance()
72
	{
73
		reset_instance();
74
		CIPHPUnitTest::createCodeIgniterInstance();
75
		$this->CI =& get_instance();
76
	}
77
78
    /**
79
     * Create a controller instance
80
     *
81
     * @param string $classname
82
     * @return CI_Controller
83
     */
84
    public function newController($classname)
85
    {
86
        reset_instance();
87
        $controller = new $classname;
88
        $this->CI =& get_instance();
89
        return $controller;
90
    }
91
92
	protected function tearDown()
93
	{
94
		if (class_exists('MonkeyPatch', false))
95
		{
96
			if (MonkeyPatchManager::isEnabled('FunctionPatcher'))
97
			{
98
				try {
99
					MonkeyPatch::verifyFunctionInvocations();
100
				} catch (Exception $e) {
101
					MonkeyPatch::resetFunctions();
102
					throw $e;
103
				}
104
105
				MonkeyPatch::resetFunctions();
106
			}
107
108
			if (MonkeyPatchManager::isEnabled('ConstantPatcher'))
109
			{
110
				MonkeyPatch::resetConstants();
111
			}
112
113
			if (MonkeyPatchManager::isEnabled('MethodPatcher'))
114
			{
115
				try {
116
					MonkeyPatch::verifyMethodInvocations();
117
				} catch (Exception $e) {
118
					MonkeyPatch::resetMethods();
119
					throw $e;
120
				}
121
122
				MonkeyPatch::resetMethods();
123
			}
124
		}
125
	}
126
127
	/**
128
	 * Request to Controller
129
	 *
130
	 * @param string       $http_method HTTP method
131
	 * @param array|string $argv        array of controller,method,arg|uri
132
	 * @param array        $params      POST parameters/Query string
133
	 */
134
	public function request($http_method, $argv, $params = [])
135
	{
136
		return $this->request->request($http_method, $argv, $params);
137
	}
138
139
	/**
140
	 * Request to Controller using ajax request
141
	 *
142
	 * @param string       $http_method HTTP method
143
	 * @param array|string $argv        array of controller,method,arg|uri
144
	 * @param array        $params      POST parameters/Query string
145
	 */
146
	public function ajaxRequest($http_method, $argv, $params = [])
0 ignored issues
show
Coding Style introduced by
ajaxRequest uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
147
	{
148
		$_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest';
149
		return $this->request($http_method, $argv, $params);
150
	}
151
152
	/**
153
	 * Get Mock Object
154
	 *
155
	 * $email = $this->getMockBuilder('CI_Email')
156
	 *	->setMethods(['send'])
157
	 *	->getMock();
158
	 * $email->method('send')->willReturn(TRUE);
159
	 *
160
	 *  will be
161
	 *
162
	 * $email = $this->getDouble('CI_Email', ['send' => TRUE]);
163
	 *
164
	 * @param  string $classname
165
	 * @param  array  $params             [method_name => return_value]
166
	 * @param  bool   $enable_constructor enable constructor or not
167
	 * @return object PHPUnit mock object
168
	 */
169
	public function getDouble($classname, $params, $enable_constructor = false)
170
	{
171
		return $this->double->getDouble($classname, $params, $enable_constructor);
172
	}
173
174
	/**
175
	 * Verifies that method was called exactly $times times
176
	 *
177
	 * $loader->expects($this->exactly(2))
178
	 * 	->method('view')
179
	 * 	->withConsecutive(
180
	 *		['shop_confirm', $this->anything(), TRUE],
181
	 * 		['shop_tmpl_checkout', $this->anything()]
182
	 * 	);
183
	 *
184
	 *  will be
185
	 *
186
	 * $this->verifyInvokedMultipleTimes(
187
	 * 	$loader,
188
	 * 	'view',
189
	 * 	2,
190
	 * 	[
191
	 * 		['shop_confirm', $this->anything(), TRUE],
192
	 * 		['shop_tmpl_checkout', $this->anything()]
193
	 * 	]
194
	 * );
195
	 *
196
	 * @param object $mock   PHPUnit mock object
197
	 * @param string $method
198
	 * @param int    $times
199
	 * @param array  $params arguments
200
	 */
201
	public function verifyInvokedMultipleTimes($mock, $method, $times, $params = null)
202
	{
203
		$this->double->verifyInvokedMultipleTimes(
204
			$mock, $method, $times, $params
205
		);
206
	}
207
208
	/**
209
	 * Verifies a method was invoked at least once
210
	 *
211
	 * @param object $mock   PHPUnit mock object
212
	 * @param string $method
213
	 * @param array  $params arguments
214
	 */
215
	public function verifyInvoked($mock, $method, $params = null)
216
	{
217
		$this->double->verifyInvoked($mock, $method, $params);
218
	}
219
220
	/**
221
	 * Verifies that method was invoked only once
222
	 *
223
	 * @param object $mock   PHPUnit mock object
224
	 * @param string $method
225
	 * @param array  $params arguments
226
	 */
227
	public function verifyInvokedOnce($mock, $method, $params = null)
228
	{
229
		$this->double->verifyInvokedOnce($mock, $method, $params);
230
	}
231
232
	/**
233
	 * Verifies that method was not called
234
	 *
235
	 * @param object $mock   PHPUnit mock object
236
	 * @param string $method
237
	 * @param array  $params arguments
238
	 */
239
	public function verifyNeverInvoked($mock, $method, $params = null)
240
	{
241
		$this->double->verifyNeverInvoked($mock, $method, $params);
242
	}
243
244
	public function warningOff()
245
	{
246
		$this->_error_reporting = error_reporting(
247
			E_ALL & ~E_WARNING & ~E_NOTICE
248
		);
249
	}
250
251
	public function warningOn()
252
	{
253
		error_reporting($this->_error_reporting);
254
	}
255
256
	/**
257
	 * Asserts HTTP response code
258
	 * 
259
	 * @param int $code
260
	 */
261
	public function assertResponseCode($code)
262
	{
263
		$status = $this->request->getStatus();
264
		$actual = $status['code'];
265
266
		$this->assertSame(
267
			$code,
268
			$actual,
269
			'Status code is not ' . $code . ' but ' . $actual . '.'
270
		);
271
	}
272
273
	/**
274
	 * Asserts HTTP response header
275
	 * 
276
	 * @param string $name  header name
277
	 * @param string $value header value
278
	 */
279
	public function assertResponseHeader($name, $value)
280
	{
281
		$CI =& get_instance();
282
		$actual = $CI->output->get_header($name);
283
284
		if ($actual === null)
285
		{
286
			$this->fail("The '$name' header is not set.\nNote that `assertResponseHeader()` can only assert headers set by `\$this->output->set_header()`");
287
		}
288
289
		$this->assertEquals(
290
			$value,
291
			$actual,
292
			"The '$name' header is not '$value' but '$actual'."
293
		);
294
	}
295
296
	/**
297
	 * Asserts HTTP response cookie
298
	 * 
299
	 * @param string       $name            cookie name
300
	 * @param string|array $value           cookie value|array of cookie params
301
	 * @param bool         $allow_duplicate whether to allow duplicated cookies
302
	 */
303
	public function assertResponseCookie($name, $value, $allow_duplicate = false)
304
	{
305
		$CI =& get_instance();
306
		$cookies = isset($CI->output->_cookies[$name])
307
			? $CI->output->_cookies[$name] : null;
308
309
		if ($cookies === null)
310
		{
311
			$this->fail("The cookie '$name' is not set.\nNote that `assertResponseCookie()` can only assert cookies set by `\$this->input->set_cookie()`");
312
		}
313
314
		$count = count($cookies);
315
		if ($count > 1 && ! $allow_duplicate)
316
		{
317
			$values = [];
318
			foreach ($cookies as $key => $val)
319
			{
320
				$values[] = "'{$val['value']}'";
321
			}
322
			$values = implode(' and ', $values);
323
			$this->fail("You have more than one cookie '$name'. The values are $values.\nIf it is okay, please set `true` as the 3rd argument of `assertResponseCookie()`");
324
		}
325
326
		// Get the last cookie
327
		$cookie = $cookies[$count - 1];
328
		if (is_string($value))
329
		{
330
			$this->assertEquals(
331
				$value,
332
				$cookie['value'],
333
				"The cookie '$name' value is not '$value' but '{$cookie['value']}'."
334
			);
335
			return;
336
		}
337
338
		foreach ($value as $key => $val)
339
		{
340
			$this->assertEquals(
341
				$value[$key],
342
				$cookie[$key],
343
				"The cookie '$name' $key is not '{$value[$key]}' but '{$cookie[$key]}'."
344
			);
345
		}
346
	}
347
348
	/**
349
	 * Asserts Redirect
350
	 * 
351
	 * @param string $uri  URI to redirect
352
	 * @param int    $code response code
353
	 */
354
	public function assertRedirect($uri, $code = null)
355
	{
356
		$status = $this->request->getStatus();
357
358
		if ($status['redirect'] === null)
359
		{
360
			$this->fail('redirect() is not called.');
361
		}
362
363
		if (! function_exists('site_url'))
364
		{
365
			$CI =& get_instance();
366
			$CI->load->helper('url');
367
		}
368
369
		if (! preg_match('#^(\w+:)?//#i', $uri))
370
		{
371
			$uri = site_url($uri);
372
		}
373
		$absolute_url = $uri;
374
		$expected = 'Redirect to ' . $absolute_url;
375
376
		$this->assertSame(
377
			$expected,
378
			$status['redirect'],
379
			'URL to redirect is not ' . $expected . ' but ' . $status['redirect'] . '.'
380
		);
381
382
		if ($code !== null)
383
		{
384
			$this->assertSame(
385
				$code,
386
				$status['code'],
387
				'Status code is not ' . $code . ' but ' . $status['code'] . '.'
388
			);
389
		}
390
	}
391
}
392