.phpt 文件结构

现在我们知道了如何使用 run-tests 运行测试,让我们更详细地研究一下 phpt 文件。phpt 文件只是一个普通的 PHP 文件,但它包含 run-tests 支持的许多不同部分。

基本测试示例

这是测试 echo 构造的 PHP 源测试的基本示例。

--TEST--
echo - basic test for echo language construct
--FILE--
<?php
echo 'This works ', 'and takes args!';
?>
--EXPECT--
This works and takes args!

您知道 echo 可以接受参数列表 吗?现在您知道了。

phpt 文件中还有 许多其他部分 可供我们使用,但这三个部分是最低要求。 --EXPECT-- 部分有一些变化,但我们稍后会介绍这些部分。

请注意,在这三个部分中,我们拥有运行黑盒测试所需的一切。我们有测试名称、一些代码和预期输出。同样,黑盒测试不关心代码如何运行,它只关心最终结果。

一些值得注意的部分

现在我们已经了解了每个 .phpt 文件所需的三个部分,让我们来看看我们肯定会遇到的其他几个常见部分。

--TEST--

测试名称

–TEST– section 仅用一行文字描述测试内容(针对人类)。这将在运行测试时显示在控制台中,因此最好能描述清楚,但不要过于冗长。如果测试需要更长的描述,可添加 -DESCRIPTION-section 部分。

--TEST--
json_decode() with large integers

--TEST-- 部分必须是 phpt 文件的第一行。否则 run-tests 将不会认为它是一个有效的测试文件,并将测试标记为 “borked”。

--FILE--

要运行的 PHP 代码

FILE- section 是我们要测试的 PHP 代码。在上面的示例中,我们要确保 echo 结构可以接收参数列表并将其连接为标准输出。

--FILE--
<?php
$json = '{"largenum":123456789012345678901234567890}';
$x = json_decode($json);
var_dump($x->largenum);
$x = json_decode($json, false, 512, JSON_BIGINT_AS_STRING);
var_dump($x->largenum);
echo "Done\n";
?>

虽然在用户区省略结尾的 PHP 标记 (?>) 被认为是一种最佳做法,但 phpt 文件并非如此。如果省略了结尾的 PHP 标记,run-tests 在运行测试时不会有任何问题,但测试将无法再像正常的 PHP 文件那样运行。这也会让你的集成开发环境发疯。因此,请务必记住在每个 --FILE-- 部分都包含关闭的 PHP 标记。

--EXPECT--

期望的输出

–EXPECT– section 包含我们期望从标准输出中看到的内容。如果您期望像在 PHPUnit 中那样获得花哨的断言,那么您在这里不会得到任何断言。请记住,这些是 “功能测试”,因此我们只是在提供输入后检查输出。

--EXPECT--
float(1.2345678901235E+29)
string(30) "123456789012345678901234567890"
Done

运行测试会根据预期输出和实际输出修剪尾随的新行,因此您不必担心在 --EXPECT-- 部分末尾添加或删除尾随的新行。

--EXPECTF--

替换后的预期输出

由于测试需要在多种环境中运行,我们常常不知道脚本的实际输出是什么。或者也许您测试的功能是不确定的。对于此用例,我们有 –EXPECTF– section,它允许我们用替换字符替换输出部分,就像 PHP 中的 sprintf() 函数一样。

--EXPECTF--
string(%d) "%s"
Done

这在创建输出 PHP 文件的绝对路径的错误案例测试时特别方便;这会因环境而异。

下面是一个简化的错误案例示例,取自对使用 --EXPECTF-- 部分的 密码哈希函数实际测试

--TEST--
Test error operation of password_hash() with bcrypt hashing
--FILE--
<?php
var_dump(password_hash("foo", PASSWORD_BCRYPT, array("cost" => 3)));
?>
--EXPECTF--
Warning: password_hash(): Invalid bcrypt cost parameter specified: 3 in %s on line %d
NULL
--SKIPIF--

应跳过测试的条件

由于 PHP 可以配置无数选项,因此您正在运行的 PHP 版本可能未使用运行测试所需的依赖项进行编译。最常见的情况是扩展测试。

如果测试需要安装扩展才能运行测试,则将有一个 –SKIPIF– section,用于检查扩展是否确实已安装。

--SKIPIF--
<?php if (!extension_loaded('json')) die('skip ext/json must be installed'); ?>

满足 --SKIPIF-- 条件的任何测试都将被 run-tests 标记为 “跳过”,并继续执行队列中的下一个测试。当您从 run-tests 运行测试时,单词 “skip” 后面的任何文本都将在输出中返回,作为跳过测试的原因。

如果满足 --SKIPIF-- 条件,许多测试将使用 die()exit() 停止脚本执行,如上例所示。重要的是要明白,仅仅因为您在 --SKIPIF-- 部分中 die(),并不意味着 run-tests 会跳过您的测试。Run-tests 只是检查 --SKIPIF-- 的输出并查找单词 “skip” 作为前四个字符。如果第一个单词不是 “skip”,则不会跳过测试。

事实上,只要 “skip” 是输出的第一个单词,您根本不必停止执行。

以下示例将跳过测试。请注意,我们没有停止脚本执行。

--SKIPIF--
<?php if (!extension_loaded('json')) echo 'skip'; ?>

相比之下,请检查以下示例。请注意它如何停止脚本执行,但由于单词 “skip” 不是输出中的第一个单词,因此 run-tests 仍会顺利运行测试而不会跳过它。

--SKIPIF--
<?php if (!extension_loaded('json')) exit; ?>

虽然不需要在 --SKIPIF-- 部分暂停脚本执行,但始终强烈建议这样做,以便您仍然可以将 phpt 文件作为普通 php 文件运行,并看到 “必须安装 skip ext/json” 之类的友好消息,而不是收到大量随机错误。

--INI--

有时测试依赖于非常具体的 INI 设置。在这种情况下,您可以使用 –INI– section 定义任何 INI 设置。每个 INI 设置都放在该部分的新行上。

--INI--
date.timezone=America/Chicago

Run-tests 为您完成设置 INI 配置的所有神奇工作。

编写简单测试

让我们编写第一个测试,以熟悉该过程。

通常,测试存储在我们要测试的代码附近的 tests/ 目录中。例如, PDO 扩展位于 PHP 源代码中的 ext/pdo。如果打开该目录,您将看到一个 tests/directory,其中包含大量 .phpt 文件。所有其他扩展都以相同的方式设置。 Zend/tests/ 中还有 Zend 引擎的测试。

对于此示例,我们将在根 php-src 目录中临时创建一个测试。使用您最喜欢的编辑器创建并打开一个新文件。

$ vi echo_basic.phpt

如果你以前从未使用过 vim,那么运行上述命令后你可能会陷入困境。只需多次按 <esc>,然后输入 :q!,它就会带你回到终端。你可以使用你最喜欢的编辑器来代替 vim 来完成这一部分。然后当你有空闲时间时,学习 vim

现在将上面的示例测试复制并粘贴到新的测试文件中。以下是测试文件,可帮您省去滚动浏览的时间。

--TEST--
echo - basic test for echo language construct
--FILE--
<?php
echo 'This works ', 'and takes args!';
?>
--EXPECT--
This works and takes args!

在 PHP 源代码根目录下将文件保存为 echo_basic.phpt,并退出编辑器后,使用 make 运行示例测试。

$ make test TESTS=echo_basic.phpt

如果一切顺利,您将看到以下通过的测试摘要。

=====================================================================
Running selected tests.
PASS echo - basic test for echo language construct [echo_basic.phpt]
=====================================================================
Number of tests :    1                 1
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Expected fail   :    0 (  0.0%) (  0.0%)
Tests passed    :    1 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    0 seconds
=====================================================================

注意测试的 --TEST-- 部分的文本是如何在控制台中显示的:

PASS echo - basic test for echo language construct [echo_basic.phpt]

为了说明黑盒测试只关心输出,让我们更改 --FILE-- 部分中的 PHP 代码,其余部分保持不变。

<?php
const BANG = '!';
class works {}
echo sprintf('This %s and takes args%s', works::class, BANG);
?>

现在让我们再次运行测试。

$ make test TESTS=echo_basic.phpt

测试应该仍然会通过,因为预期输出仍然与之前相同。让我们尝试另一个示例。将测试的 --FILE-- 部分中的 PHP 代码替换为以下代码,然后再次运行测试。

<?php
$url = 'https://gist.githubusercontent.com/SammyK/9c7bf6acdc5bcaa2cfbb404adc61abe6/';
$url .= 'raw/04af30473fc78033f7d8941ecd567934b0f804c0/foo-phpt-output.txt';
echo file_get_contents($url);
?>

虽然这个看起来不太清楚,但我设置了一个具有预期输出的 Gist,我们只是将 HTTP 请求的主体转储到该 Gist。除非出现网络连接问题或删除了 Gist,否则这将产生与其他代码相同的输出,并且测试仍将通过。如果您没有安装 ext/openssl 扩展,这将失败,因为 Gist 位于 https 后面。

让我们再试一个例子。将 --FILE-- 部分中的 PHP 代码替换为以下内容。

<?php
ob_start();

echo 'and ';
sleep(1);
echo 'takes ';
sleep(1);
echo 'args!';

$foo = ob_get_contents();
ob_clean();

echo 'This works ';
sleep(1);
echo $foo;
?>

太疯狂了吧?输出一个简单的字符串需要几秒钟的时间,而且在现实生活中你永远不会这样做,但测试仍然会通过。运行测试并不关心你的代码是慢(超时:运行测试的默认超时为 60 秒(或测试内存泄漏时为 300 秒),但您可以使用 --set-timeout 标志指定不同的超时。)还是效率低下,或者只是很糟糕,如果预期输出与实际输出相匹配,你的测试就会通过。