简述

Mock PHP的内建函数一般有两种方法:命名空间法和依赖注入法。

命名空间法是利用PHP优先使用同命名空间内函数的特性,在测试对象的命名空间内重载内建函数来实现。前提是内建函数在被调用时没有使用命名空间,例如:\debug_backtrace()是不能使用本方法的。

上篇文章《接口编程中记录日志的最佳实践》中实现的日志类中,核心部分调用了debug_backtrace()函数获取方法调用堆栈。下面讨论下如何利用命名空间法实现对此函数的mock。

实现

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<?php
namespace Ox3f\LaravelUtils\Log;

use PHPUnit\Framework\TestCase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Log as LaravelLog;
use Ox3f\LaravelUtils\Log\Log;

$calledInController = false;

function debug_backtrace() {
global $calledInController;
if ($calledInController) {
return json_decode('[{"file":"\/Users\/xbot\/Sites\/sample-project\/vendor\/xbot\/laravel-utils\/src\/Log\/Log.php","line":85,"function":"parseCallStack","class":"Ox3f\\\LaravelUtils\\\Log\\\Log","object":{},"type":"->"},{"file":"\/Users\/xbot\/Sites\/sample-project\/app\/Api\/V1\/Controllers\/WorkController.php","line":29,"function":"saveInput","class":"Ox3f\\\LaravelUtils\\\Log\\\Log","type":"::"},{"function":"save","class":"App\\\Api\\\V1\\\Controllers\\\WorkController","object":{},"type":"->"},{"file":"\/Users\/xbot\/Sites\/sample-project\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/Controller.php","line":55,"function":"call_user_func_array"},{"file":"\/Users\/xbot\/Sites\/sample-project\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/ControllerDispatcher.php","line":44,"function":"callAction","class":"Illuminate\\\Routing\\\Controller","object":{},"type":"->"}]', true);
} else {
return json_decode('[{"file":"\/Users\/xbot\/Sites\/sample-project\/vendor\/xbot\/laravel-utils\/src\/Log\/Log.php","line":85,"function":"parseCallStack","class":"Ox3f\\\LaravelUtils\\\Log\\\Log","object":{},"type":"->"},{"file":"\/Users\/xbot\/Sites\/sample-project\/app\/Notation.php","line":21,"function":"saveInput","class":"Ox3f\\\LaravelUtils\\\Log\\\Log","type":"::"},{"file":"\/Users\/xbot\/Sites\/sample-project\/app\/Api\/V1\/Controllers\/NotationController.php","line":32,"function":"incrNo","class":"App\\\Notation","type":"::"},{"function":"save","class":"App\\\Api\\\V1\\\Controllers\\\NotationController","object":{},"type":"->"},{"file":"\/Users\/xbot\/Sites\/sample-project\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/Controller.php","line":55,"function":"call_user_func_array"}]', true);
}
}

class LogTest extends TestCase
{
public function setUp()
{
Auth::shouldReceive('user')
->once()
->andReturn((object)['name' => 'jim',]);
}

/**
* @covers Ox3f\LaravelUtils\Log\Log::saveInput
* @covers Ox3f\LaravelUtils\Log\Log::saveOutput
* @covers Ox3f\LaravelUtils\Log\Log::parseCallStack
* @covers Ox3f\LaravelUtils\Log\Log::getInstance
* @covers Ox3f\LaravelUtils\Log\Log::__construct
* @covers Ox3f\LaravelUtils\Log\Log::__callStatic
*/
public function testAll()
{
global $calledInController;

// test being called in a plain method
$calledInController = false;

LaravelLog::shouldReceive('debug')
->once()
->with('jim | App\Notation::incrNo | Input:1');

Log::saveInput(1);

LaravelLog::shouldReceive('debug')
->once()
->with('jim | App\Notation::incrNo | Output:2');

Log::saveOutput(2);

// test being called in a controller action
$calledInController = true;

Request::shouldReceive('path')
->once()
->andReturn('api/user');
Request::shouldReceive('except')
->once()
->with('_url')
->andReturn(['id' => 18,]);
LaravelLog::shouldReceive('debug')
->once()
->with('jim | api/user | Input:{"id":18}');

Log::saveInput();

Request::shouldReceive('path')
->once()
->andReturn('api/user');
LaravelLog::shouldReceive('debug')
->once()
->with('jim | api/user | Output:2');

Log::saveOutput(2);

Request::shouldReceive('path')
->once()
->andReturn('api/user');
LaravelLog::shouldReceive('error')
->once()
->with('jim | api/user | this is an error');

Log::error('this is an error');

$this->assertEquals(0, 0);
}
}

最重要的是第一行,这里把测试类的命名空间设置得和测试对象一致,这样在下面重载的debug_backtrace()函数就会在测试对象中被优先使用。

在重载的函数中,通过全局变量$calledInController选择输出事先捕获的真实数据,从而mock出符合我们需要的函数。