来 ! 玩玩PHPUnit的数据库测试 (上)

前言我一生的文章都会放在这里,我的博客,我希望每一行代码,每一段文字都能帮助你。https://github.com/CrazyCodes…大家好,我是CrazyCodes,今天我们来聊聊50%(不完全统计,不必纠结比例 ????)的程序员都感觉没有啥用的数据库测试。实际测试是重中之重,正常下来一个需求应当先写测试用例后实现功能代码,如果没有在开发前做测试,那你可以选择写一个错误的断言,使用错误断言来验证代码是否符合预期,而不是根据功能去写测试,这是写测试的一种逆向思维。啥是数据库测试?很多人可能玩过单元测试,设定呀,断言呀,等等条件。但单元测试具有局限性,现如今大部分代码与数据库耦合度较高,无法独立进行单元测试,例如要做了登录模块,大概逻辑如下那可以用单元测试的地方有哪些呢?对用户名添加正则表达式(或者是规则)进行单元测试对用户密码添加正则表达式(或者是规则)进行单元测试但是否发现验证用户是否存在就无法使用单元测试进行预期的判断了?这时候就需要做数据库测试了,数据库测试实际很简单,大概的流程如下我们不看官方文档的例子,因为那对新人来说很多名词难于理解,如果你准备好了,那接下来,让我们通过实操来初试数据库测试吧!准备测试数据在准备数据前,来看看PHPUnit为我们准备的几种测试数据文件的格式。Flat XML DataSet (平直 XML 数据集)<?xml version=“1.0” ?><dataset> <user id=“1” username=“zhangsan” password=“12345” created=“2019-03-25 17:15:23” /> <user id=“2” username=“lisi” password=“12345” created=“2019-03-25 12:14:20” /></dataset>就是如上这样,上述的结构大概意思如下user 表名username user表内的username字段password user表内的pssword字段created user表内的created字段每一个以 /> 结束为一条测试数据XML DataSet (XML 数据集)<?xml version=“1.0” ?><dataset> <table name=“user”> <column>id</column> <column>username</column> <column>password</column> <column>created</column> <row> <value>1</value> <value>zhangsan</value> <value>123456</value> <value>2019-03-25 17:15:23</value> </row> <row> <value>2</value> <value>lisi</value> <value>123456</value> <value>2019-03-25 17:15:23</value> </row> </table></dataset>上述XML与第一个XML表达形式不同,但大概是一个意思首先声明了使用哪张表 <table name=“user”> 每行数据的包裹标签为 <row> <value> 标签一一对应标签 <column> 就跟SQL语句UPDATE table_name SET field1=new-value1, field2=new-value2一样。什么?你需要创造的测试数据太多?一个一个填会不会累死?那下面就是你的福音了MySQL XML DataSet (MySQL XML 数据集)Unit 可直接使用MySQL导出的数据集,你可以在MySQL控制台使用命令mysqldump –xml -t -u [username] –password=[password] [database] > /path/to/file.xml这是直接导出指定库的所有表数据,如果想指定库你可以这样做mysqldump –xml -t -u [username] –password=[password] [database] [table1] [table2] > /path/to/file.xml导出的数据大概如下<?xml version=“1.0”?><mysqldump xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance"> <database name=“testdatabase”> <table_data name=“user”> <field name=“id”>1</field> <field name=“username”>zhangsan</field> <field name=“password”>123456</field> <field name=“created”>2019-03-25 17:15:23</field> </table_data> </database></mysqldump>YAML DataSet (YAML 数据集)user: - id: 1 username: “zhangsan” password: “123456” created: 2019-03-25 17:15:23 - id: 2 username: “lisi” password:“123456” created: 2019-03-25 17:15:23相信看到这里,我不用解释你也能看懂了。其他更多的文件格式请参照 https://phpunit.readthedocs.i…并不是你喜好哪个格式就用哪个,要根据业务来,通过上面的几种方式,我们可以看出,类似于动态的数据,例如字段 created 我们不需要他是一个固定的值,而是根据时间变化,这种情况你只能让世界上最好的语言 PHP来帮你了。<?phpuse PHPUnit\Framework\TestCase;use PHPUnit\DbUnit\TestCaseTrait;class IsUserTest extends TestCase{ use TestCaseTrait; protected function getDataSet() { return new UserTest( [ ‘user’ => [ [ ‘id’ => 1, ‘username’ => ‘zhangsan’, ‘password’ => ‘123456’, ‘created’ => ‘2019-03-25 17:15:23’ ], [ ‘id’ => 2, ‘username’ => ’lisi’, ‘password’ => ‘123456’, ‘created’ => ‘2019-03-25 17:15:23’ ], ], ] ); }}?>当然你需要实现一个自定义的数据库测试类,官方提供的这个已经够用了,你也可以随意更改以达到你的测试目的<?phpclass UserTest extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet{ /** * @var array / protected $tables = []; /* * @param array $data */ public function __construct(array $data) { foreach ($data AS $tableName => $rows) { $columns = []; if (isset($rows[0])) { $columns = array_keys($rows[0]); } $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns); $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData); foreach ($rows AS $row) { $table->addRow($row); } $this->tables[$tableName] = $table; } } protected function createIterator($reverse = false) { return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse); } public function getTable($tableName) { if (!isset($this->tables[$tableName])) { throw new InvalidArgumentException("$tableName is not a table in the current database.”); } return $this->tables[$tableName]; }}?>准备验证数据验证数据与测试数据格式一样。都是官方文档的那几种。例如你希望插入数据库后的结果是<?xml version=“1.0” ?><dataset> <user id=“1” username=“zhangsan” password=“12345” created=“2019-03-25 17:15:23” /> <user id=“2” username=“lisi” password=“12345” created=“2019-03-25 12:14:20” /></dataset>那在执行测试时,unit则会将该xml文件对比数据库中的数据。一样则通过测试。就是这么简单。致谢充分掌握上述的格式以及官方文档内的demo,概念等,才能将数据库掌握在自己手中。下一章我会根据上述准备的数据准备一次“实战演习”,我已经帮你开了头,剩下的就看你自己的了。感谢你看到这里,希望本篇文章可以帮到你。 ...

March 25, 2019 · 2 min · jiezi

Yii2中使用phpunit进行测试

Yii2中使用phpunit进行测试安装 phpunit , 将 phpunit 命令添加到环境变量中 ;(详情请参照phpunit官网)进入到项目文件夹中,新建 test 文件夹, test 和 vendor,backend 同级。(可以自行定义目录结构)将 phpunit.phar 放入 test 文件夹中 。(方便测试代码中的断言调试)(可省)新建 index.php , 具体内容如下 :<?php//定义模式defined(‘YII_DEBUG’) or define(‘YII_DEBUG’, true);defined(‘YII_ENV’) or define(‘YII_ENV’, ‘dev’);//引入文件require DIR . ‘/../vendor/autoload.php’;require DIR . ‘/../vendor/yiisoft/yii2/Yii.php’;require DIR . ‘/../common/config/bootstrap.php’;//配置信息,这里默认使用的为后台配置,可自行修改$config = yii\helpers\ArrayHelper::merge( //配置信息,数据库信息 require DIR . ‘/../common/config/main.php’, require DIR . ‘/../common/config/main-local.php’, require DIR . ‘/../backend/config/main.php’);//(new yii\web\Application($config))->run() ;//注册框架基本服务,不运行框架(new yii\web\Application($config)) ;//所有测试文件 以 Test.php结尾 eg: UnitTest.php//所有测试方法 以 test开头 eg: testPay()//测试 以index.php为运行组件运行测试文件//window + R 输入 cmd ;//进入到项目中的test文件夹//运行:phpunit –bootstrap ./index.php demo/UnitTest//解释:使用 phpunit 以 index.php 为组件,运行 demo 文件夹下的 UnitTest.php 中的所有测试代码目录结构如下 : ...

March 13, 2019 · 1 min · jiezi

使用 PHPUnit 进行单元测试并生成代码覆盖率报告

安装PHPUnit使用 Composer 安装 PHPUnit#查看composer的全局bin目录 将其加入系统 path 路径 方便后续直接运行安装的命令composer global config bin-dir –absolute#全局安装 phpunitcomposer global require –dev phpunit/phpunit#查看版本phpunit –version使用Composer构建你的项目我们将新建一个unit项目用于演示单元测试的基本工作流创建项目结构mkdir unit && cd unit && mkdir app tests reports#结构如下./├── app #存放业务代码├── reports #存放覆盖率报告└── tests #存放单元测试使用Composer构建工程#一路回车即可composer init#注册命名空间vi composer.json… “autoload”: { “psr-4”: { “App\”: “app/”, “Tests\”: “tests/” } }…#更新命名空间composer dump-autoload#安装 phpunit 组件库composer require –dev phpunit/phpunit到此我们就完成项目框架的构建,下面开始写业务和测试用例。编写测试用例创建文件app/Example.php 这里我为节省排版就不写注释了<?phpnamespace App;class Example{ private $msg = “hello world”; public function getTrue() { return true; } public function getFalse() { return false; } public function setMsg($value) { $this->msg = $value; } public function getMsg() { return $this->msg; }}创建相应的测试文件tests/ExampleTest.php<?phpnamespace Tests;use PHPUnit\Framework\TestCase as BaseTestCase;use App\Example;class ExampleTest extends BaseTestCase{ public function testGetTrue() { $example = new Example(); $result = $example->getTrue(); $this->assertTrue($result); } public function testGetFalse() { $example = new Example(); $result = $example->getFalse(); $this->assertFalse($result); } public function testGetMsg() { $example = new Example(); $result = $example->getTrue(); // $result is world not big_cat $this->assertEquals($result, “hello big_cat”); }}执行单元测试[root@localhost unit]# phpunit –bootstrap=vendor/autoload.php \tests/PHPUnit 6.5.14 by Sebastian Bergmann and contributors…F 3 / 3 (100%)Time: 61 ms, Memory: 4.00MBThere was 1 failure:1) Tests\ExampleTest::testGetMsgFailed asserting that ‘hello big_cat’ matches expected true./opt/unit/tests/ExampleTest.php:27/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:195/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:148FAILURES!Tests: 3, Assertions: 3, Failures: 1.这是一个非常简单的测试用例类,可以看到,执行了共3个测试用例,共3个断言,共1个失败,可以参照PHPUnit手册学习更多高级用法。代码覆盖率代码覆盖率反应的是测试用例对测试对象的行,函数/方法,类/特质的访问率是多少(PHP_CodeCoverage 尚不支持 Opcode覆盖率、分支覆盖率 及 路径覆盖率),虽然有很多人认为过分看重覆盖率是不对的,但我们初入测试还是俗气的追求一下吧。测试覆盖率的检测对象是我们的业务代码,PHPUnit通过检测我们编写的测试用例调用了哪些函数,哪些类,哪些方法,每一个控制流程是否都执行了一遍来计算覆盖率。PHPUnit 的覆盖率依赖 Xdebug,可以生成多种格式:–coverage-clover <file> Generate code coverage report in Clover XML format.–coverage-crap4j <file> Generate code coverage report in Crap4J XML format.–coverage-html <dir> Generate code coverage report in HTML format.–coverage-php <file> Export PHP_CodeCoverage object to file.–coverage-text=<file> Generate code coverage report in text format.–coverage-xml <dir> Generate code coverage report in PHPUnit XML format.同时需要使用 –whitelist dir参数来设定我们需要检测覆盖率的业务代码路径,下面演示一下具体操作:phpunit --bootstrap vendor/autoload.php --coverage-html=reports/ --whitelist app/ \tests/#查看覆盖率报告cd reports/ && php -S 0.0.0.0:8899这样我们就对业务代码App\Example做单元测试,并且获得我们单元测试的代码覆盖率,现在自然是百分之百,因为我的测试用例已经访问了App\Example的所有方法,没有遗漏的,开发中则能体现出你的测试时用力对业务代码测试度的完善性。基境共享测试数据可能你会发现我们在每个测试方法中都创建了App\Example对象,在一些场景下是重复劳动,为什么不能只创建一次然后供其他测试方法访问呢?这需要理解 PHPUnit 执行测试用例的工作流程。我们没有办法在不同的测试方法中通过某成员属性来传递数据,因为每个测试方法的执行都是新建一个测试类对象,然后调用相应的测试方法。即测试的执行模式并不是testObj = new ExampleTest();testObj->testMethod1();testObj->testMethod2();而是testObj1 = new ExampleTest();testObj1->testMethod1();testObj2 = new ExampleTest();testObj2->testMethod2();所以testMethod1()修改的属性状态无法传递给 testMethod2()使用。PHPUnit则为我们提供了全面的hook接口:public static function setUpBeforeClass()/tearDownAfterClass()//测试类构建/解构时调用protected function setUp()/tearDown()//测试方法执行前/后调用protected function assertPreConditions()/assertPostConditions()//断言前/后调用当运行测试时,每个测试类大致就是如下的执行步骤#测试类基境构建setUpBeforeClass#new一个测试类对象#第一个测试用例setUpassertPreConditionsassertPostConditionstearDown#new一个测试类对象#第二个测试用例setUpassertPreConditionsassertPostConditionstearDown…#测试类基境解构tearDownAfterClass所以我们可以在测试类构建时使用setUpBeforeClass创建一个 App\Example 对象作为测试类的静态成员变量(tearDownAfterClass主要用于一些资源清理,比如关闭文件,数据库连接),然后让每一个测试方法用例使用它:<?phpnamespace Tests;use App\Example;use PHPUnit\Framework\TestCase as BaseTestCase;class ExampleTest extends BaseTestCase{ // 类静态属性 private static $example; public static function setUpBeforeClass() { self::$example = new Example(); } public function testGetTrue() { // 类的静态属性更新 self::$example->setMsg(“hello big_cat”); $result = self::$example->getTrue(); $this->assertTrue($result); } public function testGetFalse() { $result = self::$example->getFalse(); $this->assertFalse($result); } /** * 依赖 testGetTrue 执行完毕 * @depends testGetTrue * @return [type] [description] / public function testGetMsg() { $result = self::$example->getMsg(); $this->assertEquals($result, “hello big_cat”); }}或者使用@depends注解来声明二者的执行顺序,并使用传递参数的方式来满足需求。public function testMethod1(){ $this->assertTrue(true); return “hello”;}/* * @depends testMethod1 */public function testMethod2($str){ $this->assertEquals(“hello”, $str);}#执行模式大概如下testObj1 = new Test;$str = testObj1->testMethod1();testObj2 = new Test;testObj2->testMethod2($str);理解测试执行的模式还是很有帮助的,其他高级特性请浏览官方文档。使用phpunit.xml编排测试套件使用测试套件来管理测试,vi phpunit.xml:<?xml version=“1.0” encoding=“UTF-8”?><phpunit backupGlobals=“false” backupStaticAttributes=“false” bootstrap="./vendor/autoload.php" colors=“true” convertErrorsToExceptions=“true” convertNoticesToExceptions=“true” convertWarningsToExceptions=“true” processIsolation=“false” stopOnFailure=“false”> <testsuites> <!–可以定义多个 suffix 用于指定待执行的测试类文件后缀–> <testsuite name=“Tests”> <directory suffix=“Test.php”>./test</directory> </testsuite> </testsuites> <filter> <whitelist processUncoveredFilesFromWhitelist=“true”> <!–可以定义多个 对./app下的业务代码做覆盖率统计–> <directory suffix=".php">./app</directory> </whitelist> </filter> <logging> <!–覆盖率报告生成类型和输出目录 lowUpperBound低覆盖率阈值 highLowerBound高覆盖率阈值–> <log type=“coverage-html” target="./reports" lowUpperBound=“35” highLowerBound=“70”/> </logging></phpunit>然后直接运phpunit行即可:[root@localhost unit]# phpunit PHPUnit 6.5.14 by Sebastian Bergmann and contributors.Time: 81 ms, Memory: 4.00MBNo tests executed!Generating code coverage report in HTML format … done ...

March 7, 2019 · 2 min · jiezi

Laravel 测试: PHPUnit 入门教程

介绍 PHPUnit 测试的基础知识,使用基本的 PHPUnit 断言和 Laravel 测试助手。介绍PHPUnit 是最古老和最著名的 PHP 单元测试包之一。它主要用于单元测试,这意味着可以用尽可能小的组件测试代码,但是它也非常灵活,可以用于很多不仅仅是单元测试。PHPUnit 包含许多简单和灵活的断言允许您轻松地测试代码,当您测试特定的组件时,这些断言非常有效。但是,它确实意味着测试更高级的代码(如控制器和表单提交验证)可能会复杂得多。为了帮助开发人员更容易地进行开发, Laravel 框架 包含了一系列 应用程序测试帮助程序 ,允许您编写非常简单的 PHPUnit 测试来测试应用程序的复杂部分。本教程的目的是向您介绍 PHPUnit 测试的基础知识,使用默认 PHPUnit 断言和 Laravel 测试助手。这样做的目的是在本教程结束时,您可以自信地为应用程序编写基本测试。前提本教程假设您已经熟悉 Laravel 并知道如何在应用程序目录中运行命令(例如 php artisan 命令)。我们将创建几个基本的示例类来学习不同的测试工具如何工作,因此建议您为本教程创建一个新的应用程序。如果已经安装了 Laravel ,则可以通过运行以下命令创建新的测试应用程序:laravel new phpunit-tests或者,您可以直接使用 Composer 创建新应用程序:composer create-project laravel/laravel –prefer-dist其他安装方法也可以在 Laravel 文档中找到。创建一个新的测试使用 PHPUnit 的第一步是创建一个新的测试类。测试类的约定是它们存储在应用程序目录的 ./tests/ 下。在这个文件夹中,每个测试类都被命名为 <name>Test.php 。这种格式允许 PHPUnit 查找每个测试类—它将忽略任何不以 Test.php 结尾的文件。在新的 Laravel 应用程序中,你会注意到 ./tests/ 目录中有两个文件: ExampleTest.php 和 TestCase.php. TestCase.php 文件是一个引导文件用于在我们的测试中设置 Laravel 环境。这允许我们在测试中使用 Laravel Facades 并为测试助手提供框架,我们将在稍后介绍。 ExampleTest.php 是一个示例测试类,其中包含使用应用程序测试助手的基本测试用例-暂时忽略它。要创建一个新的测试类,我们可以手动创建一个新文件,或者运行由 Laravel 提供的 Artisan 命令 make:test 为了创建一个名为 BasicTest 的测试类,我们只需要运行这个 artisan 命令:php artisan make:test BasicTestLaravel 将创建一个如下所示的基本测试类:<?phpclass BasicTest extends TestCase{ /** * 一个基本的测试示例。 * * @return void / public function testExample() { $this->assertTrue(true); }}这里要注意的最重要的事情是 test 方法名称上的前缀,与 Test 类名后缀一样,这样 test 前缀告诉 PHPUnit 在测试时运行哪些方法。如果您忘记了 test 前缀,那么 PHPUnit 将忽略该方法。在我们第一次运行测试套件之前,有必要指出 Laravel 提供的默认 phpunit.xml 文件。 PHPUnit 在运行时会自动在当前目录中查找名为 phpunit.xml 或者 phpunit.xml.dist 的文件。您可以在此处配置测试的特定选项。这个文件中有很多信息,但是现在最重要的部分是在 testsuite 目录定义:<?xml version=“1.0” encoding=“UTF-8”?><phpunit … > <testsuites> <testsuite name=“Application Test Suite”> <directory>./tests/</directory> </testsuite> </testsuites> …</phpunit>这将告诉 PHPUnit 运行时在 ./tests/ 目录中找到的测试,正如我们之前所知,这是存储测试的约定。现在我们已经创建了一个基本测试,并且知道了 PHPUnit 配置,现在是第一次运行测试的时候了。您可以通过运行以下 phpunit 命令来运行测试:./vendor/bin/phpunit您应该看到与此类似的输出:PHPUnit 4.8.19 by Sebastian Bergmann and contributors…Time: 103 ms, Memory: 12.75MbOK (2 tests, 3 assertions)现在我们已经有了一个有效的 PHPUnit 设置,现在是时候开始编写一个基本测试了。注意,它会统计2个测试和3个断言,因为 ExampleTest.php 文件包含了一个带有两个断言的测试。我们的新基本测试包括一个单独的断言,该断言已通过。写一个基础测试为了帮助 PHPUnit 提供的基本断言,我们将首先创建一个提供一些简单功能的基本类在 ./app/ 目录中创建一个名为 Box.php 的新文件,并复制此示例类:<?phpnamespace App;class Box{ /* * @var array / protected $items = []; /* * 使用给定项构造框 * * @param array $items / public function __construct($items = []) { $this->items = $items; } /* * 检查指定的项目是否在框中。 * * @param string $item * @return bool / public function has($item) { return in_array($item, $this->items); } /* * 从框中移除项,如果框为空,则为 null 。 * * @return string / public function takeOne() { return array_shift($this->items); } /* * 从包含指定字母开头的框中检索所有项目。 * * @param string $letter * @return array / public function startsWith($letter) { return array_filter($this->items, function ($item) use ($letter) { return stripos($item, $letter) === 0; }); }}接下来, 打开你的 ./tests/BasicTest.php 类(我们之前创建的类),并删除默认创建的 testExample 方法, 你应该留一个空类。我们现在将使用七个基本的 PHPUnit 断言来为我们的 Box 类编写测试。这些断言是:assertTrue()assertFalse()assertEquals()assertNull()assertContains()assertCount()assertEmpty()assertTrue() 和 assertFalse()assertTrue() 和 assertFalse() 允许你声明一个值等于 true 或 false 。这意味着它们非常适合测试返回布尔值的方法。在我们的 Box 类中,我们有一个名为 has($item) 的方法,当指定的项在 box 中或不在 box 中时,该方法返回对应返回 true 或 false .要在 PHPUnit 中为此编写测试,我们可以执行以下操作:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); }}注意我们如何只将一个参数传递给 assertTrue() 和 assertFalse() 方法,并且它是 has($item) 方法的输入.如果您现在运行 ./vendor/bin/phpunit 命令,您会注意到输出包括:OK (2 tests, 4 assertions)这意味着我们的测试已经通过。如果您将 assertFalse() 替换成 assertTrue() 并运行 phpunit 命令,输出将如下所示:PHPUnit 4.8.19 by Sebastian Bergmann and contributors.F.Time: 93 ms, Memory: 13.00MbThere was 1 failure:1) BasicTest::testHasItemInBoxFailed asserting that false is true../tests/BasicTest.php:12FAILURES!Tests: 2, Assertions: 4, Failures: 1.这告诉我们第12行的断言未能断言 false 值是 true - 因为我们将 assertFalse() 替换为 assertTrue() 。将其交换回来,然后重新运行 PHPUnit 。测试应该再次通过,因为我们已经修复了破损的测试。assertEquals() 与 assertNull()接下来,让我们看看 assertEquals(), 以及 assertNull()。assertEquals() 用于比较变量实际值与预期值是否相等。我们用它来检查 takeOne() 方法的返回值是否为 Box 内的当前值。当 Box 为空时,takeOne() 将返回 null,我们亦可使用 assertNull() 来进行检查。与 assertTrue()、assertFalse() 以及 assertNull() 不同,assertEquals() 需要两个参数。第一个参数为 预期 值,第二个参数则为 实际 值。可参照如下代码实现以上断言(assertions):<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // 当前 Box 为空,应当为 Null $this->assertNull($box->takeOne()); }}运行 phpunit 命令,你应当看到如下输出:OK (3 tests, 6 assertions)assertContains() 和 assertCount() 以及 assertEmpty()终于,我们有三个作用于数组有关的断言,我们能够使用它们去检查 Box 类中的 startsWith($item) 方法。 assertContains() 断言传递进来的数组中包含指定值, assertCount() 断言数组的项数为指定数量,assertEmpty() 断言传递进来的数组为空。让我们来执行以下测试:<?phpuse App\Box;class BasicTest extends TestCase{ public function testHasItemInBox() { $box = new Box([‘cat’, ’toy’, ’torch’]); $this->assertTrue($box->has(’toy’)); $this->assertFalse($box->has(‘ball’)); } public function testTakeOneFromTheBox() { $box = new Box([’torch’]); $this->assertEquals(’torch’, $box->takeOne()); // Null,现在这个 box 是空的。 $this->assertNull($box->takeOne()); } public function testStartsWithALetter() { $box = new Box([’toy’, ’torch’, ‘ball’, ‘cat’, ’tissue’]); $results = $box->startsWith(’t’); $this->assertCount(3, $results); $this->assertContains(’toy’, $results); $this->assertContains(’torch’, $results); $this->assertContains(’tissue’, $results); // 如果传递复数断言数组为空 $this->assertEmpty($box->startsWith(’s’)); }}保存并再一次运行你的测试:OK (4 tests, 9 assertions)恭喜你,你刚刚使用七个基础的 PHPUnit 断言完成了对 Box 类的全部测试。通过这些简单的断言你能够做许多事,对于其他断言,大多数要更复杂,不过它们仍遵循以上使用规则。测试你的程序在你的程序里,对每个组件进行单元测试在很多情况下都是有必要的,而且也应该成为你开发过程中必不可少的一部分,但这并不是你需要做的全部的测试。当你构建一个包含复杂视图、导航和表单的程序时,你同样想测试这些组件。这时,Laravel的测试助手可以使这些测试像单元测试简单组件一样容易。我们之前查看在 ./tests/ 目录下的默认文件时跳过了 ./tests/ExampleTest.php 文件。 现在打开它,内容如下所示:<?phpclass ExampleTest extends TestCase{ /** 一个基本功能测试示例。** @return void*/ public function testBasicExample() { $this->visit(’/’) ->see(‘Laravel 5’); }}我们可以看到这个测试示例非常简单。在不知道测试助手如何运作的情况下,我们可以猜测它的意思如下:当我访问/ (根目录)我应该看到 ‘Laravel 5’如果你打开你的web浏览器,访问我们的程序(如果你没有启动你的web服务器,你可以运行 php artisan serve ),你应该可以在web根目录上看到屏幕上有“Laravel 5”的文本。 鉴于这个测试已经通过了PHPUnit,我们可以很确定地说我们对这个测试示例改造是正确的。这个测试确保了访问/路径,网页可以返回“‘Laravel 5”的文本。一个如此简单的检查也许不代表什么,但如果你的网站上要显示关键信息,它就可以在一个别处的改动导致这个页面无法正常显示正确的信息时,防止你部署一个被损坏的程序。visit()、see() 以及 dontSee()现在尝试编写自己的测试,更进一步理解它吧。首先,编辑 ./app/Http/routes.php ,增加一个新的路由。为了教程目的,我们创建希腊字母定义的路由:<?phpRoute::get(’/’,function () { return view(‘welcome’);});Route::get(’/alpha’,function () { return view(‘alpha’);});然后,创建视图文件 ./resources/views/alpha.blade.php,使用 Alpha 作为关键字,保存基本的HTML文件:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> </body></html>打开浏览器,输入网址: http://localhost:8000/beta,页面会显示出 “This is the Alpha page.” 的内容。现在我们有了测试用到的模版文件,下一步,我们通过运行命令 make:test 来创建一个新的测试文件:php artisan make:test AlphaTest然后变成刚创建好的测试文件,按照框架提供的例子,测试 “alpha” 页面上没有包含 “beta” 。 我们可以使用方法 dontSee() ,它是 see() 的对应的反向方法。下面代码是上面实现的简单例子:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); }}保存并运行 PHPUnit (./vendor/bin/phpunit),测试代码应该会全部通过,你会看到像这样的测试状态内容显示:OK (5 tests,12 assertions)开发前先写测试对于测试来说,测试驱动开发 (TDD) 是非常酷的方法,首先我们先写测试。写完测试并执行它们,你会发现测试没通过,接下来 我们编写满足测试的代码,再次执行测试,使测试通过。 接下来让我们开始。首先,建立一个 BetaTest 类使用 make:test artisan 命令:php artisan make:test BetaTest接下来,更新测试用例以便检查 /beta 的路由 route 为「Beta」:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); }}现在使用 ./vendor/bin/phpunit 命令来执行测试。结果是一个看起来简洁但不好的错误信息,如下:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…..F.Time: 144 ms, Memory: 14.25MbThere was 1 failure:1) BetaTest::testDisplaysBeta一个对 [http://localhost/beta] 的请求失败了。收到状态码 [404]。…FAILURES!Tests: 6, Assertions: 13, Failures: 1.我们现在需要创建这个不存在的路由。让我们开始。首先,编辑 ./app/Http/routes.php 文件来创建新的 /beta 路由:<?phpRoute::get(’/’, function () { return view(‘welcome’);});Route::get(’/alpha’, function () { return view(‘alpha’);});Route::get(’/beta’, function () { return view(‘beta’);});接下来,在 ./resources/views/beta.blade.php 下创建如下视图模版:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> </body></html>现在再一次执行 PHPUnit,结果应该再一次回到绿色。> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors…….Time: 142 ms, Memory: 14.00MbOK (6 tests, 15 assertions)这样我们就通过在完成新的页面之前写测试的方式,对 测试驱动开发 进行了实践。click() 和 seePageIs()Laravel 也提供一个辅助函数 (click()) 允许测试点击页面中存在的连接 ,以及一个方法 (seePageIs()) 检查点击展示的结果页面。让我们使用这两个辅助函数去执行在 Alpha 和 Beta 页面的链接。首先,我们更新我们的测试。打开 AlphaTest 类,我们将添加一个新的测试方法,这将点击 「alpha」页面上的「Next」链接跳转到 「beta」页面。新的测试代码如下:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}注意到,在我们新建的 testClickNextForBeta() 方法中,我们并没有检查每一个页面的内容。 其他测试都成功的检查了两个页面的内容,所以这里我们只关心点击 「Next」链接将发送到 /beta。你现在可以运行测试组件了,但就像预料的一样测试将不通过,因为我们还没有更新我们的 HTML。接下来,我们将更新 BetaTest 来做类似的事情:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}接下来,我们更新我们的 HTML 模版。./resources/views/alpha.blade.php:<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>./resources/views/beta.blade.php:<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>保存文件,再一次执行 PHPUnit:> ./vendor/bin/phpunitPHPUnit 4.8.19 by Sebastian Bergmann and contributors.F….F..Time: 175 ms, Memory: 14.00MbThere were 2 failures:1) AlphaTest::testDisplaysAlphaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Alpha</title> </head> <body> <p>This is the Alpha page.</p> <p><a href="/beta">Next</a></p> </body></html>’ does not match PCRE pattern “/Beta/i”.2) BetaTest::testDisplaysBetaFailed asserting that ‘<!DOCTYPE html><html> <head> <title>Beta</title> </head> <body> <p>This is the Beta page.</p> <p><a href="/alpha">Previous</a></p> </body></html>’ does not match PCRE pattern “/Alpha/i”.FAILURES!Tests: 8, Assertions: 23, Failures: 2.然而测试失败了。如果你仔细观察我们的新 HTML,你将注意到我们分别有术语 beta 和 alpha 在 /alpha 和 /beta 页面。这意味着我们需要稍微更改我们的测试让它们与误报不匹配。在每一个 AlphaTest 和 BetaTest 类,更新 testDisplays* 方法去使用 dontSee(’<page> page’)。通过这种方式,这将仅仅匹配字符串而不是那个术语。两个测试文件如下所示:./tests/AlphaTest.php:<?phpclass AlphaTest extends TestCase{ public function testDisplaysAlpha() { $this->visit(’/alpha’) ->see(‘Alpha’) ->dontSee(‘Beta page’); } public function testClickNextForBeta() { $this->visit(’/alpha’) ->click(‘Next’) ->seePageIs(’/beta’); }}./tests/BetaTest.php:<?phpclass BetaTest extends TestCase{ public function testDisplaysBeta() { $this->visit(’/beta’) ->see(‘Beta’) ->dontSee(‘Alpha page’); } public function testClickNextForAlpha() { $this->visit(’/beta’) ->click(‘Previous’) ->seePageIs(’/alpha’); }}再一次运行你的测试,所有的测试都应该通过了。我们现在已经测试我们所有的新文件,包括页面中的 Next/Previous 链接。通过 Semaphore 对 PHPUnit 持续集成通过 Semaphore设置 持续集成你可以自动执行你的测试。这样每一次你进行 git push 提交代码的时候都会执行你的测试,并且 Semaphore 预装了所有最新的 PHP 版本。如果你还没有一个 Semaphore 账户, 先去 注册一个免费的 Semaphore 账户 。接下来需要做的是将它 添加到你的项目,并按照提示逐步去做来执行你的测试:composer install –prefer-sourcephpunit关于 PHP 持续集成 的更多信息,请参照 Semaphore 文档。结语你应该注意到本教程中的所有测试都有一个共同的主题:它们都非常简单。 这是学习如何使用基本的测试断言和辅助函数,并且尽可能的使用它们的好处之一。编写测试越简单,测试就越容易理解和维护。掌握了本教程中介绍的 PHPUnit 断言之后,你还可以去 PHPUnit 文档 找到更多内容。 所有的断言都遵循基本的模式,但你会发现,在大多数测试中都会返回基本的断言。对于 PHPUnit 断言来说,Laravel 的测试辅助函数是极好的补充,这让应用程序的测试变的非常容易。也就是说,重要的是要认识到,对于我们写测试,我们只检查关键信息,而不是整个页面。这使得测试变得简单,并允许页面内容随着应用程序的变化而变化。如果关键信息仍然存在,测试仍然通过,每个人都会满意。文章转自: https://learnku.com/laravel/t… 更多文章:https://learnku.com/laravel/c… ...

March 1, 2019 · 5 min · jiezi

PHPUnit实践三(构建模块化的测试单元)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架目录结构模块下的目录是符合Lumen的模块结构的如:Controllers、Models、Logics等是Lumen模块目录下的结构目录如果有自己的目录同级分配即可,如我这里的Requests整体结构├── BaseCase.php 重写过Lumen基类的测试基类,用于我们用这个基类做测试基类,后续会说明├── bootstrap.php tests自动加载文件├── Cases 测试用例目录│ └── Headline 某测试模块│ ├── logs 日志输出目录│ ├── PipeTest.php PHPUnit流程测试用例│ ├── phpunit.xml phpunit配置文件xml│ └── README.md 本模块测试用例说明├── ExampleTest.php 最原始测试demo└── TestCase.php Lumen自带的测试基类某模块的目录结构Headline //某测试模块测试用例目录├── Cache├── Controllers│ ├── ArticleTest.php│ ├── …├── Listeners│ └── MyListener.php├── Logics├── Models│ ├── ArticleTest.php│ ├── …├── README.md├── Requests│ ├── ArticleTest.php│ ├── …├── logs //日志和覆盖率目录│ ├── html│ │ ├── …│ │ └── index.html│ ├── logfile.xml│ ├── testdox.html│ └── testdox.txt├── phpunit-debug-demo.xml //phpunit.xml案例├── phpunit-debug.xml //改名后测试用的└── phpunit.xml //正式用的xml配置BaseCase.php<?phpnamespace Test;use Illuminate\Database\Eloquent\Factory;class BaseCase extends TestCase{ protected $seeder = false; const DOMAIN = “http://xxx.com”; const API_URI = []; const TOKEN = [ ’local’ => ’token*’, ‘dev’ => ’token*’, ‘prod’ => ’’ //如果测试真实请填写授权token ]; /** * 重写setUp / public function setUp() { parent::setUp(); $this->seeder = false; if (method_exists($this, ‘factory’)) { $this->app->make(‘db’); $this->factory($this->app->make(Factory::class)); if (method_exists($this, ‘seeder’)) { if (!method_exists($this, ‘seederRollback’)) { dd(“请先创建seederRollback回滚方法”); } $this->seeder = true; $this->seeder(); } } } /* * 重写tearDown / public function tearDown() { if ($this->seeder && method_exists($this, ‘seederRollback’)) { $this->seederRollback(); } parent::tearDown(); } /* * 获取地址 * @param string $apiKey * @param string $token * @return string / protected function getRequestUri($apiKey = ’list’, $token = ‘dev’, $ddinfoQuery = true) { $query = “?token=” . static::TOKEN[strtolower($token)]; if ($ddinfoQuery) { $query = $query . “&” . http_build_query(static::DDINFO); } return $apiUri = static::DOMAIN . static::API_URI[$apiKey] . $query; }}phpunit-debug-demo.xml本文件是我们单独为某些正在测试的测试用例,直接编写的xml,可以不用来回测试,已经测试成功的测试用例了,最后全部编写完测试用例,再用正式phpunit.xml即可,具体在运行测试阶段看如何指定配置<?xml version=“1.0” encoding=“UTF-8”?><phpunit bootstrap="../../bootstrap.php" convertErrorsToExceptions=“true” convertNoticesToExceptions=“false” convertWarningsToExceptions=“false” colors=“true”> <filter> <whitelist processuncoveredfilesfromwhitelist=“true”> <directory suffix=".php">../../../app/Http/Controllers/Headline</directory> <directory suffix=".php">../../../app/Http/Requests/Headline</directory> <directory suffix=".php">../../../app/Models/Headline</directory> <exclude><file>../../../app/Models/Headline/ArticleKeywordsRelationModel.php</file> </exclude> </whitelist> </filter> <testsuites> <testsuite name=“Headline Test Suite”> <directory>./</directory> </testsuite> </testsuites> <php> <ini name=“date.timezone” value=“PRC”/> <env name=“APP_ENV” value=“DEV”/> </php> <logging> <log type=“coverage-html” target=“logs/html/” lowUpperBound=“35” highLowerBound=“70”/> <log type=“json” target=“logs/logfile.json”/> <log type=“tap” target=“logs/logfile.tap”/> <log type=“junit” target=“logs/logfile.xml” logIncompleteSkipped=“false”/> <log type=“testdox-html” target=“logs/testdox.html”/> <log type=“testdox-text” target=“logs/testdox.txt”/> </logging> <listeners> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–<string>April</string>–> <!–<double>19.78</double>–> <!–<null/>–> <!–<object class=“stdClass”/>–> <!–</arguments>–> <!–</listener>–> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–</arguments>–> <!–</listener>–> </listeners></phpunit>测试用例案例<?php/* * Created by PhpStorm. * User: qikailin * Date: 2019-01-29 * Time: 11:57 /namespace Test\Cases\Headline\Articles;use App\Http\Controllers\Headline\ArticleController;use App\Models\Headline\ArticleCategoryRelationModel;use App\Models\Headline\ArticleContentModel;use App\Models\Headline\ArticleKeywordsRelationModel;use App\Models\Headline\ArticlesModel;use Faker\Generator;use Illuminate\Http\Request;use Test\BaseCase;class ArticleTest extends BaseCase{ private static $model; public static function setUpBeforeClass() { parent::setUpBeforeClass(); self::$model = new ArticlesModel(); } /* * 生成factory faker 数据构建模型对象 * @codeCoverageIgnore / public function factory($factory) { $words = [“测试”, “文章”, “模糊”, “搜索”]; $id = 262; $factory->define(ArticlesModel::class, function (Generator $faker) use (&$id, $words) { $id++; return [ ‘id’ => $id, ‘uri’ => $faker->lexify(‘T???????????????????’), ’title’ => $id == 263 ? “搜索” : $words[rand(0, sizeof($words) - 1)], ‘authorId’ => 1, ‘state’ => 1, ‘isUpdated’ => 0, ]; }); } /* * 生成模拟的数据,需seederRollback 成对出现 / public function seeder() { $articles = factory(ArticlesModel::class, 10)->make(); foreach ($articles as $article) { // 注意: article为引用对象,不是copy if ($article->isRecommend) { $article->recommendTime = time(); } $article->save(); } } /* * getArticleList 测试数据 * @return array / public function getArticleListDataProvider() { return [ [1, “搜索”, 1, 10, 1], [2, “搜索”, 1, 10, 0], [2, null, 1, 10, 0], [3, “搜索”, 1, 10, 0], [1, null, 1, 10, 1], [2, null, 1, 10, 0], [3, null, 1, 10, 0], ]; } /* * @dataProvider getArticleListDataProvider / public function testGetArticleList($type, $searchText, $page, $pageSize, $expceted) { $rst = self::$model->getArticleList($type, $searchText, $page, $pageSize); $this->assertGreaterThanOrEqual($expceted, sizeof($rst)); $rst = self::$model->getArticleCount($type, $searchText); $this->assertGreaterThanOrEqual($expceted, $rst); } /* * addArticle 测试数据 * @return array / public function addArticleDataProvider() { return [ [ [ ‘id’ => 273, ‘uri’ => ‘dddddddddd0123’ ], ‘save’, 0 ], [ [ ‘id’ => 274, ‘uri’ => ‘dddddddddd123’ ], ‘publish’, 0 ], [ [ ‘id’ => 275, ‘uri’ => ‘dddddddddd456’ ], ‘preview’, 0 ], ]; } /* * @dataProvider addArticleDataProvider / public function testAdd($data, $action, $expected) { $rst = self::$model->addArticle($data, $action); if ($rst) { self::$model::where(‘id’, $rst)->delete(); } $this->assertGreaterThanOrEqual($expected, $rst); } public function testGetArticleInfo() { $rst = self::$model->getArticleInfo(263, 0); $this->assertGreaterThanOrEqual(1, sizeof($rst)); $rst = self::$model->getArticleInfo(2000, 1); $this->assertEquals(0, sizeof($rst)); } /* * 回滚模拟的数据到初始状态 */ public function seederRollback() { self::$model::where(‘id’, ‘>=’, 263)->where(‘id’, ‘<=’, 272)->delete(); }}运行测试cd {APPROOT}/tests/Cases/Headline# mv phpunit-debug-custom.xml -> phpunit-debug.xml../../../vendor/bin/phpunit –verbose -c phpunit-debug.xml参考PHPUnit 5.0 官方中文手册 ...

February 15, 2019 · 3 min · jiezi

phpunit入门

安装安装教程参考说明windows下安装那个盘符无所谓windows下如果不定义全局变量,可以进入phpunit.phar所在目录才能执行,例如:cd G:/phpstudy/phptutorial/tool/phpunitphpunit –version#当然配环境变量,全局配置操作更方便一点全局配置,配置环境变量的路径是“建立外包覆批处理脚本(最后得到 C:binphpunit.cmd):”官方教程里面第五步的所在路径配置配置composer参考例如{ “autoload”: { “classmap”: [ “src/” ] }, #核心 “require-dev”: { “phpunit/phpunit”: “^8” }}安装依赖composer install使用常用操作phpunit –bootstrap vendor/autoload.php tests/EmailTest //测试自己写的Email类phpunit –bootstrap vendor/autoload.php –testdox tests //testDox查看测试结果常用断言函数assertTrueassertFalseassertInstanceOfassertSameassertEqualsassertEmptyassertArrayNotHasKey示例代码zeron do php

February 1, 2019 · 1 min · jiezi

PHPUnit实践一(初识)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架前置日常我们的普通用到的测试:代码直接echo,debug等方法测试 -> 跟踪细节断点型测试log日志辅助测试 -> 跟踪细节断点型测试辅助工具,postman之类的做请求类测试->请求类测试浏览器直接测试->浏览器测试单元测试单元测试是针对程序的最小单元来进行正确性检验的测试工作,程序单元就是应用的最小可测试部件,一个单元可能是单个程序,类,对象,方法等单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在给定的场景下,有没有按照期望工作。一个场景是正向路经测试,就是在正常执行的情况下,保证代码不产生错误的测试。这种测试可以用来确认代码可以成功地向数据库中插入一条工作记录。另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。这种场景下的测试可能是对数据库进行查询时没有找到任何结果,或者对数据库做了无效的更新。在这两种情况下,测试都要验证确实产生了错误,且产生的是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的优点或改善解决问题减少bug通过运行单元测试可以直接测试各个功能的正确性,有bug可以直接发现并解决,如果要等到跟其他的功能对接,进行连贯测试,测试比较麻烦,而且bug不能及早的发现并解决快速定位bug如果是web项目的某一个功能,平常我们定位bug可能是页面输入值,后台断点,一步一步的需要bug位置,如果有编写单元测试,则可以直接修改数据,运行单元测试即可,快速有限提高代码质量如果每一个部件都是完美的,那么组合起来肯定也是完美的。整体代码质量就得到了保障减少调试时间当不知问题所在的时候,可能需要各种调试与运行,而如果所有的都有编写单元测试,那么可以直接运行单元测试,就能定位问题所在位置。PHPUnitPHPUnit是一个面向PHP程序员的测试框架,这是一个xUnit的体系结构的单元测试框架。版本主版本初始版本PHP兼容性支持后台框架对应版本PHPUnit 82019年2月1日PHP 7.2, PHP 7.3, PHP 7.4在2021年2月5日结束支持 PHPUnit 72018年2月2日PHP 7.1, PHP 7.2, PHP 7.3在2020年2月7日结束支持 PHPUnit 62017年2月3日PHP 7.0, PHP 7.1, PHP 7.2在2019年2月1日结束支持*PHPUnit 52015年10月2日PHP 5.6, PHP 7.0, PHP 7.1在2018年2月2日结束支持 PHPUnit 42014年3月7日PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6在2017年2月3日结束支持 你的第一个单元测试demo目录结构tests├── ExampleTest.php 测试用例└── TestCase.php Lumen自带测试基类,继承PHPunit代码<?phpclass ExampleTest extends TestCase{ /** * 测试断言成功. * * @return void / public function testTrue() { $this->assertTrue(true); } /* * 测试断言失败 * * @return void / public function testFailure() { $this->assertTrue(false); } /* * 测试不加断言,risky. * * @return void */ public function testRisky() { }}运行../vendor/bin/phpunit ExampleTest.php输出PHPUnit 6.5.9 by Sebastian Bergmann and contributors..FR 3 / 3 (100%)Time: 902 ms, Memory: 10.00MBThere was 1 failure:1) ExampleTest::testFailureFailed asserting that false is true./web/www/wpt/gt-api/tests/ExampleTest.php:22–There was 1 risky test:1) ExampleTest::testRiskyThis test did not perform any assertionsFAILURES!Tests: 3, Assertions: 2, Failures: 1, Risky: 1.说明3个测试方法,2个断言 一个断言失败,一个测试方法无断言输出标识说明.当测试成功时输出。F当测试方法运行过程中一个断言失败时输出。E当测试方法运行过程中产生一个错误时输出。R当测试被标记为有风险时输出。S当测试被跳过时输出。I当测试被标记为不完整或未实现时输出。参考PHPUnit 6.5 官方文档 ...

January 30, 2019 · 1 min · jiezi

PHPUnit实践二(生命周期)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架PHPUnit测试一个文件类的生命周期理解PHPUnit加载机制(Lumen版)PHPUnit自动测试文件会自动加载引入(include file)PHPUnit去启动setUp方法,Lumen里重写了setUp,加载了bootstrap/app.phpapp.php加载了composer的autoload,借此你项目所有自动加载环境都有了,不过不包含tests目录至此我们引入了我们需要构建自己的自动加载类增加tests的自动加载我们需要给tests下的测试用例创建类似下面的结构├── BaseCase.php 重写过Lumen基类的测试基类,用于我们用这个基类做测试基类,后续会说明├── bootstrap.php tests自动加载文件├── Cases 测试用例目录│ └── Demo 测试模块│ ├── logs 日志输出目录│ ├── PipeTest.php PHPUnit流程测试用例│ ├── phpunit.xml phpunit配置文件xml│ └── README.md 本模块测试用例说明├── ExampleTest.php 最原始测试demo└── TestCase.php Lumen自带的测试基类tests自动加载文件代码<?php/** * 测试框架的自动加载测试文件类 * User: qikailin /error_reporting(E_ALL ^ E_NOTICE);require DIR . ‘/../vendor/autoload.php’;define(‘MY_TESTS_DIR_BASE’, realpath(dirname(FILE)));set_include_path(implode(PATH_SEPARATOR, array( WPT_TEST_DIR_BASE, get_include_path())));spl_autoload_register(function ($class) { $classFile = MY_TESTS_DIR_BASE . DIRECTORY_SEPARATOR . str_replace([“Test\”, “/”, “\”], ["", DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $class) . “.php”; if (file_exists($classFile)) { include_once $classFile; }}, true, false);phpunit.xml自动加载配置bootstrap文件<?xml version=“1.0” encoding=“UTF-8”?><phpunit bootstrap="../../bootstrap.php" convertErrorsToExceptions=“true” convertNoticesToExceptions=“false” convertWarningsToExceptions=“false” colors=“true”></phpunit>流程测试代码PipeTest 流程代码<?php/* * 测试类的每个测试方法都会运行一次 setUp() 和 tearDown() 模板方法(同时,每个测试方法都是在一个全新的测试类实例上运行的)。 * 另外,setUpBeforeClass() 与 tearDownAfterClass() 模板方法将分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用。 * 如果有需要共享的对象或变量,可以放在setUpBeforeClass,并设置为静态属性 * User: qikailin /namespace Test\Cases\Demo;use Test\BaseCase;class PipeTest extends BaseCase{ public static function setUpBeforeClass() { fwrite(STDOUT, METHOD . “\n”); } public function setUp() { fwrite(STDOUT, METHOD . “\n”); } /* * 测试方法的前置执行,setUp之后 / protected function assertPreConditions() { fwrite(STDOUT, METHOD . “\n”); } public function testOne() { fwrite(STDOUT, METHOD . “\n”); $this->assertTrue(true); } public function testTwo() { fwrite(STDOUT, METHOD . “\n”); // 两个交换下顺序可以看下效果 // 正常执行成功assert可以继续执行,失败的会跳出方法 $this->assertArrayHasKey(’d’, [’d’=>1, ’e’=>2]); $this->assertTrue(false); } public function testThree() { fwrite(STDOUT, METHOD . “\n”); $this->assertTrue(false); } public function testFour() { fwrite(STDOUT, METHOD . “\n”); } /* * 测试方法成功后的后置执行,tearDown之前 / protected function assertPostConditions() { fwrite(STDOUT, METHOD . “\n”); } public function tearDown() { fwrite(STDOUT, METHOD . “\n”); } public static function tearDownAfterClass() { fwrite(STDOUT, METHOD . “\n”); } /* * 不成功后拦截方法 * 必须重新抛出错误,如果不抛出错误,断言会当成成功了 */ public function onNotSuccessfulTest(\Throwable $e) { fwrite(STDOUT, METHOD . “\n”); // 必须重新抛出错误,如果不抛出错误,断言会当成成功了 throw $e; }}运行# 你可以把vendor/bin加入到环境变量PATHcd tests/Demo../../../vendor/bin/phpunit运行输出PHPUnit 6.5.9 by Sebastian Bergmann and contributors.Test\Cases\Demo\PipeTest::setUpBeforeClassTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testOneTest\Cases\Demo\PipeTest::assertPostConditionsTest\Cases\Demo\PipeTest::tearDown.Test\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testTwoTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::onNotSuccessfulTestFTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testThreeTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::onNotSuccessfulTestFTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testFourTest\Cases\Demo\PipeTest::assertPostConditionsTest\Cases\Demo\PipeTest::tearDownR 4 / 4 (100%)Test\Cases\Demo\PipeTest::tearDownAfterClassTime: 1.29 seconds, Memory: 6.00MBThere were 2 failures:1) Test\Cases\Demo\PipeTest::testTwoFailed asserting that false is true./xxx/tests/Cases/Demo/PipeTest.php:472) Test\Cases\Demo\PipeTest::testThreeFailed asserting that false is true./xxx/tests/Cases/Demo/PipeTest.php:53–There was 1 risky test:1) Test\Cases\Demo\PipeTest::testFourThis test did not perform any assertionsFAILURES!Tests: 4, Assertions: 4, Failures: 2, Risky: 1.Generating code coverage report in HTML format … done整理流程输出Test\Cases\Demo\PipeTest::setUpBeforeClassTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testOneTest\Cases\Demo\PipeTest::assertPostConditionsTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testTwoTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::onNotSuccessfulTestTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testThreeTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::onNotSuccessfulTestTest\Cases\Demo\PipeTest::setUpTest\Cases\Demo\PipeTest::assertPreConditionsTest\Cases\Demo\PipeTest::testFourTest\Cases\Demo\PipeTest::assertPostConditionsTest\Cases\Demo\PipeTest::tearDownTest\Cases\Demo\PipeTest::tearDownAfterClass总结一个测试类文件,从setUpBeforeClass加载,且仅此加载一次每个测试方法都会走的过程:setUp->assertPreConditions->测试方法->[assert成功执行:assertPostConditions]->tearDown->[assert执行失败:onNotSuccessfulTest,且本方法需要抛出错误]本个测试类文件执行tearDownAfterClass结束参考PHPUnit 6.5 官方文档 ...

January 30, 2019 · 2 min · jiezi

PHPUnit 加速技巧分享

具备高效的测试一如编写高效的应用一样重要。作为开发者来说,迅速得知你刚编写的代码是否能够正常运行,能够让开发效率大大提升。接下来我们将会介绍一些可以快速实现的小技巧,让你的代码测试变得更快。该示例测试套件有意地模拟更广泛的测试集合,并突出改进的可行性。真实情况下,效率的提升可能有所差异。ParaTest这个包 是一个用来运行你的测试套件的 PHPUnit 扩展。 和 PHPUnit 不一样的是它可以利用你的多核 CPU 来并行的运行测试用例。你可以通过 composer 来将它作为一个开发依赖安装以后开始使用 ParaTest 。composer require –dev brianium/paratest现在我们就可以像调用 PHPUnit 一样来调用 ParaTest 了。它会自动的根据你机器 CPU 核心数来判断要启动多少个进程。<img src=“https://user-gold-cdn.xitu.io…;h=611&f=png&s=233798” class=“rm-style”>上面,你可以看到在控制台中输出了运行测试用例启动了5个并行的进程。对比一下,下面用 PHPUnit 运行了同样的测试用例。<img src=“https://user-gold-cdn.xitu.io…;h=177&f=png&s=20131” class=“rm-style”>1.49 秒 和 6.15秒 !尽管 ParaTest 可以自己确定进程数,你也可以尝试设置进程数针对你的机器进行优化。使用 —processes 选项,你可以增加或减少进程数,因为并不是进程数越多测试效果越好。./vendor/bin/paratest –processes 6警告: 使用 ParaTest 测试数据库前,需要考虑如何准备数据。如果使用 Lavarel 的 RefreshDatabase ,运行测试用例后会回滚或者迁移数据库来写入 。 相反的是,通过 DatabaseTransactions 跳过数据持久化, 这在运行测试期间不会尝试修改数据。重试失败的测试PHPUnit 有个非常方便的功能就是,允许你重新只运行上次测试中失败的测试.。如果你正在进行红绿复建风格的 TDD 开发,它将会加快你的开发周期。让我们从一个通过所有现存测试的测试套件来了解一下它这个功能。<img src=“https://user-gold-cdn.xitu.io…;h=162&f=png&s=13166” class=“rm-style”>接下来,新增一个 red-green-refactor 测试模型的测试用例,预期失败:<img src=“https://user-gold-cdn.xitu.io…;h=151&f=png&s=12965” class=“rm-style”>在更改代码库之后你认为新测试会通过,你想重新运行该测试套件以期能按预期运行。问题在于这个套件现在已经要花 1.3 秒的时间才能运行,随着测试代码量的增加,所需严重等待的时间也随之增加。如果我们只能运行失败的测试,那不是很好? 非常幸运的是 PHPUnit v7.3添加了这样做的能力。<img src=“https://user-gold-cdn.xitu.io…;h=151&f=png&s=11977” class=“rm-style”>为了实现这个功能,请将 cacheResult =“true” 添加到 phpunit.xml配置中。 PHPUnit 会始终记住以前哪些测试失败了。<?xml version=“1.0” encoding=“UTF-8”?><phpunit cacheResult=“true” backupGlobals=“false” …>现在,当我们运行我们的测试单元时, PHPUnit 将记住哪些测试失败并使用以下选项让我们可以重新运行那些失败的测试单元。./vendor/bin/phpunit –order-by=defects –stop-on-defect我们不再需要等待整个测试单元运行,以查看我们试图解决的一个测试是否正在通过。将缓存文件 .phpunit.result.cache 添加到 .gitignore 也是一个好主意,这样它就不会最终被提交到你的仓库里。慢测试分组PHPUnit 允许你用 @group 注解来将测试用例添加到不同的「分组」。如果你有一些测试用例尤其的慢的话,最好是将他们分到同一个组。class MyTest extends TestCase{ public function test_that_is_fast() { $this->assertTrue(true); } /** * @group slow / public function test_that_is_slow() { sleep(10); $this->assertTrue(true); } /* * @group slow */ public function test_that_is_slow_2_adrians_revenge() { sleep(10); $this->assertFalse(false); }}在这个例子中,我们有两个测试用例要运行10秒钟。 在我们开发周期内最后一件要做的事情就是运行测试用例,尤其是在做测试驱动开发的时候需要测试用例瞬时执行完成。由于两个慢的测试用例都在同一个分组所以你可以通过 PHPUnit 的 –exclude-group 选项在某一次测试运行中来排除他们。./vendor/bin/phpunit –exclude-group slow这个命令将会运行你测试用例中除了 slow 分组的所有测试用例。 测试用例分组还有一个好处,比如说你需要将你所有的慢测试用例整理成文档以便后面再来优化他们。然而在部署到生产环境前进行一些检查确保所有测试用例能通过,包括慢的测试用例。 设置一个 CI 管道来运行测试用例会是个不错的方法。过滤测试PHPUnit 有一个 –filter 选项,它接受一个模式来确定运行哪些测试。例如,如果您将所有测试配置命名空间 ,则可以通过指定命名空间来运行特定的测试子集。 以下命令仅在 Tests\Unit\Models 命名空间中运行测试并排除所有其他命令。./vendor/bin/phpunit –filter ‘Tests\Unit\Models’–filter 选项是灵活的,允许通过 methodName,Class::methodName 进行过滤,甚至可以通过带有 /path/to/my/test.php 的文件路径进行过滤。您应该查看此选项的 PHPUnit docs 并查看更多的内容。密码哈希次数Laravel 默认使用 bcrypt 密码哈希算法,这种设计在系统资源上缓慢且昂贵。如果您的测试是验证用户密码,可以通过设置算法使用的次数来减少测试运行的时间,因为它执行的次数越多,所需的时间就越长。如果你的应用程序与 laravel/laravelproject 中的最新更改保持同步,你会发现哈希次数的数量可以使用环境变量进行自定义。bcrypt 允许的最小次数已经设置为4,在 phpunit.xml file.但是,如果您没有同步最新的更新,可以使用 Hash 门面在CreatesApplication trait 中设置它。public function createApplication(){ $app = require DIR.’/../bootstrap/app.php’; $app->make(Kernel::class)->bootstrap(); // 设置 bcrypt 哈希次数… Hash::rounds(4); return $app;}内存数据库利用内存数据库 SQLite ,是另一种加速测试的方式。 你可以通过在 phpunit.xml 配置文件里添加两个环境字段,来迅速开启它。<php> … <env name=“DB_CONNECTION” value=“sqlite”/> <env name=“DB_DATABASE” value=":memory:"/></php>说明:尽管这样看上去很容易,你应该考虑生产环境数据库一致性问题。如果你在生产环境使用了 MySQL 数据库,你应该警惕引入不同数据库所带来的测试上的不同,比如 SQLite。我在这篇文章 my feature test suite setup 里描述了很多细节上的不同点。我认为相比通过提升一点速度带来的好处,保持生产环境一致更重要。禁用 Xdebug如果你平时用不到 Xdebug 的话,可以禁用掉它,因为它会降低 PHP 执行速度,导致测试用例运行缓慢。如果你日常使用它来调试的话,为了执行测试而禁用它可能不是一个好的选择 —— 但你始终要知道这一点当你关注测试用例执行速度时。你可以在下面这个测试用例看到,一旦我们禁用了 Xdebug,执行速度将会有极大的提高。下面是这个测试用例在 Xdebug 启用时的执行情况:<img src=“https://user-gold-cdn.xitu.io…;h=151&f=png&s=11703” class=“rm-style”>以及同样的测试用例在 Xdebug 禁用时的执行情况:<img src=“https://user-gold-cdn.xitu.io…;h=151&f=png&s=11374” class=“rm-style”>修复测试速度过慢当然我们最希望看到的段落是是:修复测试速度过慢!如果您正在努力确定哪些测试导致测试单元变慢时,您可能需要查看 PHPUnit Report 。它是一个开源工具,允许您通过生成如下所示的云可视化您的测试单元的性能,其中较大的气泡代表慢速测试。这将使您能够在单元中找到最慢的测试并逐步提高其性能。转自 PHP / Laravel 开发者社区 https://laravel-china.org/top… ...

January 21, 2019 · 2 min · jiezi