提高 CakePHP 3 单元测试性能
Improving CakePHP 3 Unit Test Performance
我的 CakePHP 3.5 项目的单元测试非常慢。我们有大量数据必须加载到固定装置中才能充分测试我们的应用程序。我们在 5 个不同的集成测试 classes 中加载了 50 个 fixture,并且在这些 classes 中总共进行了 78 个测试。这些 运行.
需要几分钟
fixtures 模式是从我们的数据库加载的,记录使用 records public 变量填充,示例:
class AmenityFixture extends TestFixture
{
public $table = 'amenity';
public $import = ['connection' => 'default', 'model' => 'Amenity'];
/**
* Init method
*
* @return void
*/
public function init()
{
$this->records = [
[
'id' => 1,
'amenity_category_id' => 8,
'name' => 'Air conditioning',
'slug' => 'air-conditioning',
'status' => 'active',
'created' => '2013-06-03 11:07:30',
'modified' => '2013-06-18 12:17:29'
],
是否可以只加载一次固定装置,也许通过 tests/boostrap.php 然后在测试退出后将它们拆除到其他地方?我看到过对这类事情的引用,但仅限于 Cake 2。
更多信息
我知道它的固定装置,因为如果我通过退出设置方法强制创建固定装置,然后注释掉固定装置,我们最大的集成测试控制器在 1.23 秒内测试 运行。如果必须从头开始创建夹具,则需要 2 分钟以上的时间。我想知道 Cake 是为每个测试创建固定装置还是为每个控制器创建固定装置?我只需要这些固定装置 运行 一次,即使每个控制器只 运行ning 一次(如果它们确实是 运行ning 每个测试方法)将是一个巨大的改进。
这里是一个测试的例子class和示例测试方法:
<?php
namespace Api\Test\TestCase\Controller;
use Api\Controller\BookController;
use Cake\TestSuite\IntegrationTestCase;
use Cake\Network\Http\Client;
use Cake\Utility\Security;
use Cake\Core\Configure;
use App\Logic\BookingLogic;
use App\Legacy\CodeIgniter\CI_Encrypt;
/**
* Api\Controller\DefaultController Test Case
* @example vendor/bin/phpunit plugins/Api
*/
class BookControllerTest extends IntegrationTestCase
{
/**
* Fixtures
*
* @var array
*/
public $fixtures = [
'app.amenity',
'app.amenity_category',
'app.api_log',
'app.area',
'app.area_filter',
'app.area_geocode',
'app.area_publisher',
'app.area_property',
'app.country',
'app.discount',
'app.error_log',
'app.feature',
'app.filter',
'app.organization',
'app.publisher',
'app.publisher_key',
'app.publisher_property',
'app.publisher_property_feature',
'app.publisher_property_order',
'app.promotion',
'app.promotion_blackout',
'app.promotion_property_accommodation',
'app.promotion_purchase_requirement',
'app.promotion_type',
'app.promotion_stay_requirement',
'app.property',
'app.property_accommodation',
'app.property_accommodation_publisher_exception',
'app.property_accommodation_rate',
'app.property_accommodation_rate_log',
'app.property_accommodation_rate_availability',
'app.property_accommodation_rate_availability_log',
'app.property_accommodation_image',
'app.property_accommodation_provider_attribute_value',
'app.property_accommodation_type',
'app.property_address',
'app.property_address_geocode',
'app.property_amenity',
'app.property_description',
'app.property_discount',
'app.property_discount_option',
'app.property_image',
'app.property_fee',
'app.property_filter',
'app.property_policy',
'app.property_provider',
'app.property_provider_attribute_value',
'app.property_rating',
'app.property_telephone',
'app.property_type',
'app.provider',
'app.provider_attribute',
'app.reservation',
'app.reservation_confirmation',
'app.reservation_customer',
'app.reservation_customer_address',
'app.reservation_customer_telephone',
'app.reservation_idx',
'app.reservation_payment',
'app.reservation_promotion',
'app.reservation_property_accommodation',
'app.reservation_property_accommodation_discount',
'app.reservation_property_accommodation_rate',
'app.state',
];
public function setUp(){
$this->checkin = date('Y-m-d', strtotime('+2 days'));
$this->checkout = date('Y-m-d', strtotime('+6 days'));
$this->configRequest([
'headers' => ['Accept' => 'application/json']
]);
}
public function testAvailability(){
$this->post('/publisher/v3.0/book/preview.json',json_encode([
'property_id' => 1111,
'accommodation_id' => 1303,
'rate_code' => 'REZ',
'checkin' => $this->checkin,
'checkout' => $this->checkout,
'rooms' => [
['adults' => 2, 'children' => 0],
]
]));
$this->assertEquals(958.64, $rate->total->total);
}
我更改了 unit/integration 测试的工作方式。这适用于为每个测试用例设置和拆除数据库(我可能会补充说,对于大 O 来说这是一个疯狂的禁忌)。这是一个很长的答案:
tests/bootstrap.php
我们将在 bootstrap 文件中定义我们的灯具,并使用扩展灯具管理器的新 class 构建它们(稍后会详细介绍)。在 bootstrap 文件的末尾,我们将强制连接使用我们的测试数据库。
<?php
use App\Logic\FixtureMaker;
use Cake\Datasource\ConnectionManager;
require dirname(__DIR__) . '/config/bootstrap.php';
$fixtures = [
'app.all_your_table_fixtures',
'app.right_here',
];
$connection = ConnectionManager::get('test');
$connection->disableForeignKeys();
$fixtureMaker = new FixtureMaker();
echo "\r\nBuilding fixtures\r\n";
foreach ($fixtures as $fixture) {
$table = str_replace('app.', '', $fixture);
$model = Cake\Utility\Inflector::camelize($table);
$fixture = $model . 'Fixture';
$fixtureMaker->pushFixture($model, $fixture);
$fixtureMaker->loadSingle($model, $connection, false);
}
unset($fixtureMaker); // i was getting pdo errors without this
unset($connection);
echo "\r\nFixtures completed\r\n";
ConnectionManager::alias('test', 'default');
FixtureMaker
你可以把它放在任何地方,对我们来说它在 App\Logic\FixtureMaker 命名空间中。它只需要扩展 FixtureManager,这样我们就可以使用它的受保护方法,即 loadSingle()。我们还需要填充受保护的属性 _loaded 和 _fixtureMap。很基础。
<?php
namespace App\Logic;
use Cake\TestSuite\Fixture\FixtureManager;
class FixtureMaker extends FixtureManager
{
public function pushFixture($class, $fixture)
{
$className = "App\Test\Fixture\" . $fixture;
$this->_loaded[$fixture] = new $className();
$this->_fixtureMap[$class] = $this->_loaded[$fixture];
}
}
phpunit.xml.dist
这里没有什么疯狂的,只是删除 Cake 默认放置在那里的监听器。如果这些听众留在那儿,那么 Cake 会尝试在它自己的固定装置中打耳光,我们不希望这样。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
processIsolation="false"
stopOnFailure="true"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php"
>
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
</php>
<!-- Add any additional test suites you want to run here -->
<testsuites>
<testsuite name="App Test Suite">
<directory>./tests/TestCase</directory>
<directory>./plugins/Api/tests</directory>
<directory>./plugins/Adapter/tests</directory>
</testsuite>
<!-- Add plugin test suites here. -->
</testsuites>
<!-- Setup a listener for fixtures -->
<listeners>
</listeners>
<!-- Ignore vendor tests in code coverage reports -->
<filter>
<whitelist>
<directory suffix=".php">./src/</directory>
<directory suffix=".php">./plugins/*/src/</directory>
</whitelist>
</filter>
</phpunit>
赛程
从各种测试 classes 中删除对您的装置的所有引用。你的实际固定装置 classes 可以按照你想要的方式完成,我想,对我来说,我选择读取生产模式但创建自定义记录。这是更可取的,因为我永远不必更新夹具架构。如果需要 add/edit 测试记录,我应该只需要修改现有的夹具。这是一个示例夹具:
<?php
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* PromotionTypeFixture
*
*/
class PromotionTypeFixture extends TestFixture
{
public $table = 'promotion_type';
public $import = ['connection' => 'default', 'model' => 'PromotionType'];
/**
* Init method
*
* @return void
*/
public function init()
{
$this->records = [
[
'id' => 1,
'name' => '$ off',
'created' => '2012-11-05 13:02:09'
],
[
'id' => 2,
'name' => '% off',
'created' => '2012-11-05 13:02:14'
],
[
'id' => 3,
'name' => 'Free Night',
'created' => '2012-11-05 13:02:18'
],
];
parent::init();
}
}
最后一句话
弄清楚这是一件非常痛苦的事情,但它使我们的单元测试执行时间从将近 3 分钟(并以残酷的速度增长)减少到 13 秒。我们这里确实有相当数量的测试:
时间:13.13 秒,内存:36.00MB
好的,但是测试不完整、被跳过或有风险!
测试:111,断言:802,不完整:21.
我的 CakePHP 3.5 项目的单元测试非常慢。我们有大量数据必须加载到固定装置中才能充分测试我们的应用程序。我们在 5 个不同的集成测试 classes 中加载了 50 个 fixture,并且在这些 classes 中总共进行了 78 个测试。这些 运行.
需要几分钟fixtures 模式是从我们的数据库加载的,记录使用 records public 变量填充,示例:
class AmenityFixture extends TestFixture
{
public $table = 'amenity';
public $import = ['connection' => 'default', 'model' => 'Amenity'];
/**
* Init method
*
* @return void
*/
public function init()
{
$this->records = [
[
'id' => 1,
'amenity_category_id' => 8,
'name' => 'Air conditioning',
'slug' => 'air-conditioning',
'status' => 'active',
'created' => '2013-06-03 11:07:30',
'modified' => '2013-06-18 12:17:29'
],
是否可以只加载一次固定装置,也许通过 tests/boostrap.php 然后在测试退出后将它们拆除到其他地方?我看到过对这类事情的引用,但仅限于 Cake 2。
更多信息 我知道它的固定装置,因为如果我通过退出设置方法强制创建固定装置,然后注释掉固定装置,我们最大的集成测试控制器在 1.23 秒内测试 运行。如果必须从头开始创建夹具,则需要 2 分钟以上的时间。我想知道 Cake 是为每个测试创建固定装置还是为每个控制器创建固定装置?我只需要这些固定装置 运行 一次,即使每个控制器只 运行ning 一次(如果它们确实是 运行ning 每个测试方法)将是一个巨大的改进。
这里是一个测试的例子class和示例测试方法:
<?php
namespace Api\Test\TestCase\Controller;
use Api\Controller\BookController;
use Cake\TestSuite\IntegrationTestCase;
use Cake\Network\Http\Client;
use Cake\Utility\Security;
use Cake\Core\Configure;
use App\Logic\BookingLogic;
use App\Legacy\CodeIgniter\CI_Encrypt;
/**
* Api\Controller\DefaultController Test Case
* @example vendor/bin/phpunit plugins/Api
*/
class BookControllerTest extends IntegrationTestCase
{
/**
* Fixtures
*
* @var array
*/
public $fixtures = [
'app.amenity',
'app.amenity_category',
'app.api_log',
'app.area',
'app.area_filter',
'app.area_geocode',
'app.area_publisher',
'app.area_property',
'app.country',
'app.discount',
'app.error_log',
'app.feature',
'app.filter',
'app.organization',
'app.publisher',
'app.publisher_key',
'app.publisher_property',
'app.publisher_property_feature',
'app.publisher_property_order',
'app.promotion',
'app.promotion_blackout',
'app.promotion_property_accommodation',
'app.promotion_purchase_requirement',
'app.promotion_type',
'app.promotion_stay_requirement',
'app.property',
'app.property_accommodation',
'app.property_accommodation_publisher_exception',
'app.property_accommodation_rate',
'app.property_accommodation_rate_log',
'app.property_accommodation_rate_availability',
'app.property_accommodation_rate_availability_log',
'app.property_accommodation_image',
'app.property_accommodation_provider_attribute_value',
'app.property_accommodation_type',
'app.property_address',
'app.property_address_geocode',
'app.property_amenity',
'app.property_description',
'app.property_discount',
'app.property_discount_option',
'app.property_image',
'app.property_fee',
'app.property_filter',
'app.property_policy',
'app.property_provider',
'app.property_provider_attribute_value',
'app.property_rating',
'app.property_telephone',
'app.property_type',
'app.provider',
'app.provider_attribute',
'app.reservation',
'app.reservation_confirmation',
'app.reservation_customer',
'app.reservation_customer_address',
'app.reservation_customer_telephone',
'app.reservation_idx',
'app.reservation_payment',
'app.reservation_promotion',
'app.reservation_property_accommodation',
'app.reservation_property_accommodation_discount',
'app.reservation_property_accommodation_rate',
'app.state',
];
public function setUp(){
$this->checkin = date('Y-m-d', strtotime('+2 days'));
$this->checkout = date('Y-m-d', strtotime('+6 days'));
$this->configRequest([
'headers' => ['Accept' => 'application/json']
]);
}
public function testAvailability(){
$this->post('/publisher/v3.0/book/preview.json',json_encode([
'property_id' => 1111,
'accommodation_id' => 1303,
'rate_code' => 'REZ',
'checkin' => $this->checkin,
'checkout' => $this->checkout,
'rooms' => [
['adults' => 2, 'children' => 0],
]
]));
$this->assertEquals(958.64, $rate->total->total);
}
我更改了 unit/integration 测试的工作方式。这适用于为每个测试用例设置和拆除数据库(我可能会补充说,对于大 O 来说这是一个疯狂的禁忌)。这是一个很长的答案:
tests/bootstrap.php
我们将在 bootstrap 文件中定义我们的灯具,并使用扩展灯具管理器的新 class 构建它们(稍后会详细介绍)。在 bootstrap 文件的末尾,我们将强制连接使用我们的测试数据库。
<?php
use App\Logic\FixtureMaker;
use Cake\Datasource\ConnectionManager;
require dirname(__DIR__) . '/config/bootstrap.php';
$fixtures = [
'app.all_your_table_fixtures',
'app.right_here',
];
$connection = ConnectionManager::get('test');
$connection->disableForeignKeys();
$fixtureMaker = new FixtureMaker();
echo "\r\nBuilding fixtures\r\n";
foreach ($fixtures as $fixture) {
$table = str_replace('app.', '', $fixture);
$model = Cake\Utility\Inflector::camelize($table);
$fixture = $model . 'Fixture';
$fixtureMaker->pushFixture($model, $fixture);
$fixtureMaker->loadSingle($model, $connection, false);
}
unset($fixtureMaker); // i was getting pdo errors without this
unset($connection);
echo "\r\nFixtures completed\r\n";
ConnectionManager::alias('test', 'default');
FixtureMaker
你可以把它放在任何地方,对我们来说它在 App\Logic\FixtureMaker 命名空间中。它只需要扩展 FixtureManager,这样我们就可以使用它的受保护方法,即 loadSingle()。我们还需要填充受保护的属性 _loaded 和 _fixtureMap。很基础。
<?php
namespace App\Logic;
use Cake\TestSuite\Fixture\FixtureManager;
class FixtureMaker extends FixtureManager
{
public function pushFixture($class, $fixture)
{
$className = "App\Test\Fixture\" . $fixture;
$this->_loaded[$fixture] = new $className();
$this->_fixtureMap[$class] = $this->_loaded[$fixture];
}
}
phpunit.xml.dist
这里没有什么疯狂的,只是删除 Cake 默认放置在那里的监听器。如果这些听众留在那儿,那么 Cake 会尝试在它自己的固定装置中打耳光,我们不希望这样。
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
processIsolation="false"
stopOnFailure="true"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php"
>
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
</php>
<!-- Add any additional test suites you want to run here -->
<testsuites>
<testsuite name="App Test Suite">
<directory>./tests/TestCase</directory>
<directory>./plugins/Api/tests</directory>
<directory>./plugins/Adapter/tests</directory>
</testsuite>
<!-- Add plugin test suites here. -->
</testsuites>
<!-- Setup a listener for fixtures -->
<listeners>
</listeners>
<!-- Ignore vendor tests in code coverage reports -->
<filter>
<whitelist>
<directory suffix=".php">./src/</directory>
<directory suffix=".php">./plugins/*/src/</directory>
</whitelist>
</filter>
</phpunit>
赛程
从各种测试 classes 中删除对您的装置的所有引用。你的实际固定装置 classes 可以按照你想要的方式完成,我想,对我来说,我选择读取生产模式但创建自定义记录。这是更可取的,因为我永远不必更新夹具架构。如果需要 add/edit 测试记录,我应该只需要修改现有的夹具。这是一个示例夹具:
<?php
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* PromotionTypeFixture
*
*/
class PromotionTypeFixture extends TestFixture
{
public $table = 'promotion_type';
public $import = ['connection' => 'default', 'model' => 'PromotionType'];
/**
* Init method
*
* @return void
*/
public function init()
{
$this->records = [
[
'id' => 1,
'name' => '$ off',
'created' => '2012-11-05 13:02:09'
],
[
'id' => 2,
'name' => '% off',
'created' => '2012-11-05 13:02:14'
],
[
'id' => 3,
'name' => 'Free Night',
'created' => '2012-11-05 13:02:18'
],
];
parent::init();
}
}
最后一句话
弄清楚这是一件非常痛苦的事情,但它使我们的单元测试执行时间从将近 3 分钟(并以残酷的速度增长)减少到 13 秒。我们这里确实有相当数量的测试:
时间:13.13 秒,内存:36.00MB
好的,但是测试不完整、被跳过或有风险! 测试:111,断言:802,不完整:21.