场景如下:您有将发出标头和内容的代码,例如,一个前端控制器。你如何测试这个?
答案非常简单,但并不明显:名称空间。
先决条件
对于这种工作方法,假设是:
- 您的代码发出标头和输出位于全局命名空间以外的命名空间中。
就是这样。考虑到您获取的大多数PHP代码都这样做,并且您遇到的大多数编码标准都需要这样做,可以肯定的是您已经准备好了。如果你不是,现在就去重构你的代码,然后再继续;你稍后会感谢我的。
技术
PHP在PHP5.3中引入了命名空间。正如我们大多数人都清楚的那样,命名空间涵盖类,但它们也涵盖常量和函数——这一事实经常被忽视,在5.6(下周发布!)之前,您不能通过use语句导入它们!
这并不意味着它们不能被定义和使用,但是——它只是意味着您需要手动导入它们,通常是通过require
或require_once
语句。这些在库中通常是令人厌恶的,但对于测试来说,它们工作得很好。
这是我最近采用的一种方法。我创建了一个存在的文件——这是重要的一点,所以请注意——与代码发出头文件和输出位于相同的命名空间中。该文件定义了几个位于全局(又名PHP的内置)命名空间中的函数,以及一个累加器静态对象,然后我可以在我的断言测试中使用它。这是它的样子:
namespace Some\Project; abstract class Output { public static $headers = array(); public static $body; public static function reset() { self::$headers = array(); self::$body = null; } } function headers_sent() { return false; } function header($value) { Output::$headers[] = $value; } function printf($text) { Output::$body .= $text; }
一些注意事项:
headers_sent()
在这里总是返回false,因为大多数发射器会测试布尔真值并在出现这种情况时提前退出。- 我使用了
printf()
在这里,因为echo不能被覆盖,因为它是PHP语言构造而不是实际函数。因此,如果您使用此技术,您可能必须更改发射器以调用printf()
而不是echo。然而,这样做的好处是值得的。 - 我将输出标记为抽象,以防止实例化;它只能静态使用。
我将上述文件放在我的测试套件中,通常在与测试本身相邻的TestAsset
目录下;因为它包含函数,所以我将文件命名为Functions.php
。这种组合通常会阻止它以任何方式自动加载,因为测试目录通常不会定义自动加载,或者将位于单独的命名空间下。
然后,在您的PHPUnit测试套件中,您将执行以下操作:
namespace SomeTest\Project; use PHPUnit_Framework_TestCase as TestCase; use Some\Project\FrontController; use Some\Project\Output; // <-- our Output class from above require_once __DIR__ . '/TestAsset/Functions.php'; // <-- get our functions class FrontControllerTest extends TestCase { public function setUp() { Output::reset(); /* ... */ } public function tearDown() { Output::reset(); /* ... */ } }
从这里开始,您可以像往常一样进行测试——但是当您调用将导致标头或内容发出的方法时,您现在可以测试以查看它们包含的内容:
public function testEmitsExpectedHeadersAndContent() { /* ... */ $this->assertContains('Content-Type: application/json', Output::$headers); $json = Output::$body; $data = json_decode($json, true); $this->assertArrayHasKey('foo', $data); $this->assertEquals('bar', $data['foo']); }
工作原理
为什么会这样?
PHP在解析函数时会施展魔法。对于类,它会在当前名称空间或导入的名称空间(可能有别名)中寻找匹配的类;如果未找到匹配项,它将停止并引发错误。但是,对于函数,它首先在当前命名空间中查找,如果未找到,则在全局命名空间中查找。最后一部分是关键——这意味着如果您在当前命名空间中重新定义一个函数,它将被用来代替PHP定义的原始函数。这也意味着在与该函数相同的命名空间中运行的任何代码——即使是在另一个文件中定义的——都将使用该函数。
这种技术正是利用了这一事实。