大家好,我是编程小6,很高兴遇见你,有问题可以及时留言哦。
这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击 协同翻译介绍 。
在此之前,我将假设你熟悉
PHP面向对象风格
,并且使用
PHP7以上
的版本进行开发。
为了能拥有一个正常运行的
PHP7
环境,并且可以跟随文中的步骤操作 且不出现奇怪的问题,我们推荐你使用 Homestead 作为开发环境。
本文中给出的结果实例将会是运行结束的样子,请不要担心,在操作中会有提示对你的操作进行引导。
这是一个比你想象中更加强大的工具
如果你仍在疑惑为什么我们推荐每个人使用
Vagrant Box
,以下两篇文章将可以做出解释
- Jump Start PHP Environment 快速且优质的搭建PHP环境 (English)
- introduction of Vagrant Vagrant 介绍 (English)
查看其他 1 个版本
测试驱动开发是这样一种想法,就是说在真正开始开发之前,先编写一段测试代码,用来确保我们的想法能够如愿实现。
检查在 TDD-land 中 asserting(断言,如果不理解此概念,请参考 PHP 之 assert()函数) 是否确实是我们所期望的。记住这个术语。
举个例子,一个断言 2+2=4 是正确的。但是,如果我们觉得 2+3=4,那么测试框架(如 PHPUnit)会将此断言标记为 false,我们将这个称之为“失败的测试”。我们测试 2+3=4 失败了。很显然,在你的应用中你不会去测试常量之和,相反的,你会用变量替代,这时你便会得到断言的结果。
查看其他 1 个版本
PHPUnit 是一个程序(PHP 类和可执行文件)的集合,它不仅使得编写测试变得简单(编写测试通常需要比编写实际应用的代码更多 - 但这是值得的),并且它还允许你在一个很优雅的图表中看到测试过程的输出,这个图表可以让你了解代码质量(例如,也许在一个类中有太多的 if - 这会被标记为质量差,因为有这么多的 if 就会导致在改变一个条件时需要重写很多测试代码)。
闲话少说,学习走起~
本教程的代码可以在 这里 下载。
在这个例子中,我们将创建一个简单的命令行程序包,它把JSON文件转换成PHP文件,用PHP的关联数组表示JSON数据.这其实是我日常用到的一个事例,我使用 Diffbot 很频繁,而且这些东西的输出有时候会非常之多以至于没办法人工观察处理,所以用PHP简单处理一下就解决这个问题可以说很舒服了. 从现在开始, 默认你在使用一个完全支持PHP7的环境并且安装了 Composer ,这就足够了.另外如果你在使用 Homestead Improved, 那直接
vagrant ssh
SSH进去, 现在咱们就开始吧。 第一步,进入项目目录.这个演示的例子使用 Homestead Improved,项目目录是
Code
.
cd Code
然后,我们在 PDS-Skeleton 的基础上创建一个新项目,并且在它里面使用 Composer安装PHPUnit.
git clone https://github.com/php-pds/skeleton converter
cd converter
composer require phpunit/phpunit --dev
注意,我们使用了 --dev
标识,这样可以只把PHPUnit安装成开发的依赖,在发布时不会包含在内,这样可以让发布的项目更加精简.另外还要注意,因为使用了PDS-Skeleton所以它已经创建了一个test
文件夹,里面还有两个对我们没用的待删除的demo文件.
查看其他 1 个版本
接下来,我们的应用需要一个前端控制器 -- 这个文件的所有请求都通过路由访问。在
converter/public
文件夹下,创建
index.php
,内容如下:
<?php
echo "Hello world";
你应该很熟悉这段代码。使用浏览器打开这个文件,确保可以正常访问。
如果你使用的是 Homestead,希望你可以创建一个虚拟主机,并使用虚拟主机的 IP 访问你的应用。
现在让我们删除额外的文件。你可以手动删除,也可以使用下面的命令:
rm bin/* src/* docs/* tests/*
你可能会问,为什么我们需要 Hello World 这个前端控制器?其实我们在本教程中并不会用到它,但稍后我们在测试应用的时候,它会很有用。所以不管怎样,最终它不是我们这个包的一部分。
我们需要一个配置文件来告诉 PHPUnit 去哪找到测试、在测试之前要做哪些准备,以及如何测试。在项目根目录下,创建
phpunit.xml
文件,内容如下:
<phpunit bootstrap="tests/autoload.php">
<testsuites>
<testsuite name="converter">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>
phpunit.xml
一个测试可以有多个测试套件,因业务逻辑而异。比如说,任何跟用户相关的内容都可以归类到 “users” 套件中,这可能就需要不同的测试逻辑或者存放在不同的文件夹中来测试这些功能。在我们这个示例中,项目很小,针对
tests
目录,一个套件绰绰有余。我们定义了
suffix
参数,这就意味着 PHPUnit 将只运行那些以
Test.php
结尾的文件。当我们还想在
tests
中创建其他文件时,除了从实际的 Test 文件中调用它们之外,不希望它们运行,这时这个
suffix
参数就很有用了。
你可以在 这里 了解其他关于这方面的内容。
Kevinvinvin 翻译于 2周前 1 重译
bootstrap
的值告诉 PHPUnit 在测试之前应该加载哪个 PHP 文件。这对于配置自动加载、在项目范围内测试变量,甚至是测试数据库等等(所有你在生产环境下不想要或者不需要的东西)一系列功能都是非常有用的。现在创建
tests/autoload.php
文件:
<?php
require_once __DIR__.'/../vendor/autoload.php';
tests/autoload.php
在这个例子中,我们只加载 Composer 的默认自动加载器,因为 PDS-Skeleton 已经在
composer.json
中为我们配置了测试的命名空间。如果我们用自己的文件替换了该文件中的模板值,那么最终的
composer.json
文件应该是这样的:
{
"name": "sitepoint/jsonconverter",
"type": "standard",
"description": "A converter from JSON files to PHP array files.",
"homepage": "https://github.com/php-pds/skeleton",
"license": "MIT",
"autoload": {
"psr-4": {
"SitePoint\\": "src/SitePoint"
}
},
"autoload-dev": {
"psr-4": {
"SitePoint\\": "tests/SitePoint"
}
},
"bin": ["bin/converter"],
"require-dev": {
"phpunit/phpunit": "^6.2"
}
}
现在,我们运行
composer du
(简写
dump-autoload
)来刷新自动加载脚本。
composer du
请记住,TDD 是先犯错误,再更正错误,不要搞混淆了。明白了这一点,现在就来创建第一个测试。
<?php
namespace SitePoint\Converter;
use PHPUnit\Framework\TestCase;
class ConverterTest extends TestCase {
public function testHello() {
$this->assertEquals('Hello', 'Hell' . 'o');
}
}
tests/SitePoint/Converter/ConverterTest.php
如果像我们所期望的那样,测试代码能够跟我们的项目具有相同的结构,那再好不过了。明白了这一点之后,我们给它们相同的命令空间和目录结构。因此,
ConverterTest.php
文件将在
tests
/
SitePoint
/
Converter
文件夹中了。
我们正在扩展的文件是 PHPUnit 提供的 Test 类的最基本版本。在大多数情况下,这已经足够使用了。如果还不够的话,那你可以更深层次的扩展,那就更好不过了。但是请记住 - 测试不必遵循良好的软件设计原则,所以深度的继承和代码的复用是更合理的 - 这就可以随心所欲的测试啦!
查看其他 1 个版本
这个例子 "test case" 断言 字符串
Hello
是由
Hell
和
o
相连,如果我们运行
php vendor/bin/phpunit
,我们就会得到这个通过的结果。
PHPUnit 默认会执行所有
Test
文件中方法名以
test
开头的方法,这就是为什么我们在运行测试工具时不需要明确的去指定 - 因为这是全自动的。
不过,我们目前的测试既不实用也不现实。我们只是用它来检查我们的配置是否成功。我们现在写一个合适的。像这样重写
ConverterTest.php
:
<?php
namespace SitePoint\Converter;
use PHPUnit\Framework\TestCase;
class ConverterTest extends TestCase {
public function testSimpleConversion() {
$input = '{"key":"value","key2":"value2"}';
$output = [
'key' => 'value',
'key2' => 'value2'
];
$converter = new \SitePoint\Converter\Converter();
$this->assertEquals($output, $converter->convertString($input));
}
}
tests/SitePoint/Converter/ConverterTest.php
好了,所以这里发生了什么?
我们正在测试一个简单的转换,输入一个JSON字符串,并期望它输出的是PHP数组,我们的测试断言我们的转换器类,在使用
conversionstring
方法处理
$ input
时,会产生所需的
$ output
,就像上面定义的那样。
再运行一遍测试工具。
测试失败,就像期望的一样,因为这个类还不存在。
让我们的测试看起来更好看些 -- 加入颜色! 修改 phpunit.xml
使得 <phpunit
标签包含 colors="true"
属性,就像这样:
<phpunit colors="true" bootstrap="tests/autoload.php">
现在当我们运行 php vendor/bin/phpunit
, 我们会得到一个更好看的输出:
现在我们开始进行让测试通过的操作。
我们的第一个错误是 “Class 'SitePoint\Converter\Converter' not found”。让我们来修正它。
<?php
namespace SitePoint\Converter;
class Converter {
}
src/SitePoint/Converter/Converter.php
;
现在我们重新运行测试...
有进展!我们现在调用了不存在的方法。让我们把它加进去。
<?php
namespace SitePoint\Converter;
class Converter {
public function convertString(string $input): ?array {
}
}
src/SitePoint/Converter/Converter.php
;
我们定义了一个接受字符串类型输入,返回数组或者在不成功时返回空 (null) 的方法。如果你不知道什么是标量类型(
string $input
),点击这里去学习一下, here, 还有可为空返回类型 (
?array
),可以看这里 here.
再运行一下测试。
这里有个返回错误 -- 方法没有返回任何东西 (void) -- 因为它里面是空的 -- 而且它的期望是返回空 (null) 或者数组。让我们来完善这个方法。我们将使用 PHP 的内置方法
json_decode
来完成对 JSON 字符串的转码。
public function convertString(string $input): ?array {
$output = json_decode($input);
return $output;
}
src/SitePoint/Converter/Converter.php
;
我们再次运行测试看看会发生什么
Oh!这个方法返回了一个对象,而并非一个数组。啊哈!因为我们没有在
json_decode
方法里开启 "associative array" 模式。这个方法模式将 JSON 数组转换为
stdClass
对象,除非我们另行说明。像这样:
public function convertString(string $input): ?array {
$output = json_decode($input, true);
return $output;
}
src/SitePoint/Converter/Converter.php
;
接着运行测试。
很好!我们的测试通过了!它在测试中获得了和我们所期望的完全一致的输出!
無限之秋 翻译于 2周前 1 重译 我们再多加几个测试用例,以确保我们的方法真正按预期执行。让我们比开始的简单例子做得更复杂点。那么我继续写方法
ConverterTest.php
:
{
$input = '{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}';
$output = [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5],
];
$converter = new \SitePoint\Converter\Converter();
$this->assertEquals($output, $converter->convertString($input));
}
public function testMoreComplexConversion()
{
$input = '{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}';
$output = [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5],
'new-object' => [ 'key' => 'value', 'key2' => 'value2', ],
];
$converter = new \SitePoint\Converter\Converter();
$this->assertEquals($output, $converter->convertString($input));
}
public function testMostComplexConversion()
{
$input = '[{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}]';
$output = [ [ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5],
'new-object' => [ 'key' => 'value', 'key2' => 'value2', ],
],
[ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5],
'new-object' => [ 'key' => 'value', 'key2' => 'value2', ],
],
[ 'key' => 'value', 'key2' => 'value2', 'some-array' => [1, 2, 3, 4, 5],
'new-object' => [ 'key' => 'value', 'key2' => 'value2', ],
],
];
$converter = new \SitePoint\Converter\Converter();
$this->assertEquals($output, $converter->convertString($input));
}
tests/SitePoint/Converter/ConverterTest.php
我们让每个测试用例都比以前的复杂了点,尤其最后一个用例在一个数组中包含了多个对象。重新跑一下这个测试集合,以展示一切正常……
... 不过似乎有什么不对劲对不对? 这里存在大量重复代码,如果我们要修改该类的 API,就必须要修改四个地方(就目前代码而言)。DRY原则的优势就体现出来,即使是测试中。恰好有个功能可以处理此事。
数据提供器在测试类中是很特殊的函数,它只有一个明确目标:给测试函数提供一系列的数据,以避免在多个测试函数中重复相同逻辑,就象前面做的。最好还是在示例中解释一下,我们对类
ConverterTest
进行重构:
<?php
namespace SitePoint\Converter;
use PHPUnit\Framework\TestCase;
class ConverterTest extends TestCase
{
public function conversionSuccessfulProvider()
{
return [
[
'{"key":"value","key2":"value2"}',
[
'key' => 'value',
'key2' => 'value2',
],
],
[
'{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}',
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
],
],
[
'{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}',
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
'new-object' => [
'key' => 'value',
'key2' => 'value2',
],
],
],
[
'[{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}},{"key":"value","key2":"value2","some-array":[1,2,3,4,5],"new-object":{"key":"value","key2":"value2"}}]',
[
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
'new-object' => [
'key' => 'value',
'key2' => 'value2',
],
],
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
'new-object' => [
'key' => 'value',
'key2' => 'value2',
],
],
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
'new-object' => [
'key' => 'value',
'key2' => 'value2',
],
],
],
],
];
}
/** * @param $input * @param $output * @dataProvider conversionSuccessfulProvider */
public function testStringConversionSuccess($input, $output)
{
$converter = new \SitePoint\Converter\Converter();
$this->assertEquals($output, $converter->convertString($input));
}
}
tests/SitePoint/Converter/ConverterTest.php
我先写了个命名为
conversionSuccessfulProvider
的新方法。这暗示这样的一种预期:所有提供的用例应该返回正面的结果,因为输出和输入相匹配。数据提供器返回数组(以便测试函数能自动遍历所有元素)。该数组的每个元素都是单独的测试用例 —— 我们的用例中,每个元素是个包含两个元素的数组:前者是输入元素,后者是输出元素,和前面的代码类似。
bigqiang 翻译于 2周前 1 重译 我们把这些测试功能合并到一个方法中,给该方法起一个更通用、更有所指何物的明确性的名称:
testStringConversionSuccess
。该测试方法接受两个参数: input 和 output。 其余逻辑与以前一致。 此外,为确保该方法使用了这个数据提供器,我们要在该方法的 docblock 块区中用
@dataProvider conversionSuccessfulProvider
声明该提供器。
大功造成 —— 现在我们可以得到完全相同的结果。
现在再想添加更多的测试用例,仅需要给提供器多添加些 input-output 值对即可。没必要发明新的方法名重复那些逻辑了。方便多了,对吧?
在我们看这个部分之前,让我们来吸收迄今为止所介绍的所有内容,让我们简单地讨论代码覆盖率。
代码覆盖率是一个度量标准,告诉我们有多少代码被测试覆盖。
如果我们的类有两个方法,但是只有一个在测试中被测试过,那么我们的代码覆盖率至多是50% - 取决于方法有多少个逻辑分支(if,switch,loop等)每个分支都应该有一个单独的测试覆盖)。
phpunit能够在运行给定的测试工具后自动生成代码覆盖率报告。 让我们来快速配置一下,我们会在
phpunit.xml
的
<phpunit>
里面添加
<logging>
和
<filter>
,作为1级子元素 (如果
<phpunit>
是0级或者根级元素的话):
<phpunit ...>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<logging>
<log type="tap" target="tests/build/report.tap"/>
<log type="junit" target="tests/build/report.junit.xml"/>
<log type="coverage-html" target="tests/build/coverage" charset="UTF-8" yui="true" highlight="true"/>
<log type="coverage-text" target="tests/build/coverage.txt"/>
<log type="coverage-clover" target="tests/build/logs/clover.xml"/>
</logging>
在过滤器中设置一个白名单,告诉phpunit在测试时需要注意哪些文件。这会编译成 / src里的所有.php文件,在任何级别。
日志记录会告诉phpunit要生成哪些报告 - 不同工具可以生成不同报告,因此生成更多格式的报告并不会造成什么影响。
在我们的例子中,我们只是对html作探讨
科大大 翻译于 2周前 1 重译 在这个可以工作之前,我们需要启用XDebug,因为这是PHPUnit 所需要的PHP扩展, Homestead Improved
phpenmod
工具可以在运行中启用或停用 PHP 扩展:
sudo phpenmod xdebug
如果你没有使用 HI(Homestead Improved),请遵循XDebug对应的相关系统的安装方法, 这篇文章 应该有所帮助。
重新运行该工具我们应该就能看到生成的覆盖率报告。另外,它们也会生成在指定位置的目录树中。
让我们现在用浏览器打开
index.html
,直接把它拖到浏览器中应该就好了 -- 不需要再启动服务器什么的 -- 因为这只是个静态文件。
这个文件将列出所有测试的摘要。你可以点击进入单独的类来查看详细的覆盖率报告,悬停在方法上会弹出给定方法测试的工具提示。
随着我们进一步开发我们的工具,我们将在后续文章中深入探讨代码覆盖率。
在这个PHPUnit的介绍中,我们看到了测试驱动开发(TDD) 的基本概念。接触到了一个PHP工具开始阶段的观念。所有的代码可以 从Github下载下来。
我们透过PHPUnit基础,解释数据的提供者,展示了代码覆盖率。这个帖子仅仅接触到了PHPUnit的一些基本概念和特性。我们鼓励你进一步探索。或者你提出你理解不了的需要解释的概念,我们希望能给你解释清楚。
在后面的文章中,我们将介绍一些中间技术, 进一步开发我们的应用。
请在下面留下你的评论和问题!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。