Browse Source

Merge pull request #15404 from cakephp/4.next-phpunit-ext

4.next Start data only fixtures
Mark Story 5 years ago
parent
commit
e470e3dacd

+ 1 - 0
src/TestSuite/Fixture/FixtureInjector.php

@@ -94,6 +94,7 @@ class FixtureInjector implements TestListener
      */
     public function startTest(Test $test): void
     {
+        // TODO replace this with reading from the singleton
         /** @psalm-suppress NoInterfaceProperties */
         $test->fixtureManager = $this->_fixtureManager;
         if ($test instanceof TestCase) {

+ 132 - 0
src/TestSuite/Fixture/TransactionStrategy.php

@@ -0,0 +1,132 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         4.3.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Fixture;
+
+use Cake\Database\Connection;
+use Cake\Datasource\ConnectionManager;
+use RuntimeException;
+
+/**
+ * Fixture state strategy that wraps tests in transactions
+ * that are rolled back at the end of the transaction.
+ *
+ * This strategy aims to gives good performance at the cost
+ * of not being able to query data in fixtures from another
+ * process.
+ *
+ * @TODO create a strategy interface
+ */
+class TransactionStrategy
+{
+    /**
+     * Constructor.
+     *
+     * @param bool $enableLogging Whether or not to enable query logging.
+     * @return void
+     */
+    public function __construct(bool $enableLogging = false)
+    {
+        $this->aliasConnections($enableLogging);
+    }
+
+    /**
+     * Alias non test connections to the test ones
+     * so that models reach the test database connections instead.
+     *
+     * @param bool $enableLogging Whether or not to enable query logging.
+     * @return void
+     */
+    protected function aliasConnections(bool $enableLogging): void
+    {
+        $connections = ConnectionManager::configured();
+        ConnectionManager::alias('test', 'default');
+        $map = [];
+        foreach ($connections as $connection) {
+            if ($connection === 'test' || $connection === 'default') {
+                continue;
+            }
+            if (isset($map[$connection])) {
+                continue;
+            }
+            if (strpos($connection, 'test_') === 0) {
+                $map[$connection] = substr($connection, 5);
+            } else {
+                $map['test_' . $connection] = $connection;
+            }
+        }
+        foreach ($map as $testConnection => $normal) {
+            ConnectionManager::alias($testConnection, $normal);
+            $connection = ConnectionManager::get($normal);
+            if ($connection instanceof Connection) {
+                $connection->enableSavePoints();
+                if (!$connection->isSavePointsEnabled()) {
+                    throw new RuntimeException(
+                        "Could not enable save points for the `{$normal}` connection. " .
+                            'Your database needs to support savepoints in order to use the ' .
+                            'TransactionStrategy for fixtures.'
+                    );
+                }
+                if ($enableLogging) {
+                    $connection->enableQueryLogging();
+                }
+            }
+        }
+    }
+
+    /**
+     * Before each test start a transaction.
+     *
+     * @return void
+     */
+    public function beforeTest(): void
+    {
+        $connections = ConnectionManager::configured();
+        foreach ($connections as $connection) {
+            if (strpos($connection, 'test') !== 0) {
+                continue;
+            }
+            $db = ConnectionManager::get($connection);
+            if ($db instanceof Connection) {
+                $db->begin();
+                $db->createSavePoint('__fixtures__');
+            }
+        }
+    }
+
+    /**
+     * After each test rollback the transaction.
+     *
+     * As long as the application code has balanced BEGIN/ROLLBACK
+     * operations we should end up at a transaction depth of 0
+     * and we will rollback the root transaction started in beforeTest()
+     *
+     * @return void
+     */
+    public function afterTest(): void
+    {
+        $connections = ConnectionManager::configured();
+        foreach ($connections as $connection) {
+            if (strpos($connection, 'test') !== 0) {
+                continue;
+            }
+            $db = ConnectionManager::get($connection);
+            if ($db instanceof Connection) {
+                $db->rollback(true);
+            }
+        }
+    }
+}

+ 70 - 0
src/TestSuite/FixtureSchemaExtension.php

@@ -0,0 +1,70 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         4.3.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite;
+
+use Cake\TestSuite\Fixture\TransactionStrategy;
+use PHPUnit\Runner\AfterTestHook;
+use PHPUnit\Runner\BeforeTestHook;
+
+/**
+ * PHPUnit extension to integrate CakePHP's data-only fixtures.
+ */
+class FixtureSchemaExtension implements
+    AfterTestHook,
+    BeforeTestHook
+{
+    /**
+     * @var object
+     */
+    protected $state;
+
+    /**
+     * Constructor.
+     *
+     * @param string $stateStrategy The state management strategy to use.
+     */
+    public function __construct(string $stateStrategy = TransactionStrategy::class)
+    {
+        $enableLogging = in_array('--debug', $_SERVER['argv'] ?? [], true);
+        $this->state = new $stateStrategy($enableLogging);
+
+        // TODO Create the singleton fixture manager that tests will use.
+    }
+
+    /**
+     * BeforeTestHook implementation
+     *
+     * @param string $test The test being run.
+     * @return void
+     */
+    public function executeBeforeTest(string $test): void
+    {
+        $this->state->beforeTest();
+    }
+
+    /**
+     * AfterTestHook implementation
+     *
+     * @param string $test The test being run.
+     * @param float $time The duration the test took.
+     * @return void
+     */
+    public function executeAfterTest(string $test, float $time): void
+    {
+        $this->state->afterTest();
+    }
+}

+ 2 - 0
src/TestSuite/TestCase.php

@@ -212,6 +212,8 @@ abstract class TestCase extends BaseTestCase
     {
         parent::setUp();
 
+        // TODO add loading data only fixtures.
+
         if (!$this->_configure) {
             $this->_configure = Configure::read();
         }

+ 34 - 0
tests/TestCase/TestSuite/Fixture/TransactionStrategyTest.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace Cake\Test\TestCase\TestSuite;
+
+use Cake\ORM\TableRegistry;
+use Cake\TestSuite\Fixture\TransactionStrategy;
+use Cake\TestSuite\TestCase;
+
+class TransactionStrategyTest extends TestCase
+{
+    public $fixtures = ['core.Users'];
+
+    /**
+     * Test that beforeTest starts a transaction that afterTest closes.
+     *
+     * @return void
+     */
+    public function testTransactionWrapping()
+    {
+        $users = TableRegistry::get('Users');
+
+        $strategy = new TransactionStrategy();
+        $strategy->beforeTest();
+        $user = $users->newEntity(['username' => 'testing', 'password' => 'secrets']);
+
+        $users->save($user, ['atomic' => true]);
+        $this->assertNotEmpty($users->get($user->id), 'User should exist.');
+
+        // Rollback and the user should be gone.
+        $strategy->afterTest();
+        $this->assertNull($users->findById($user->id)->first(), 'No user expected.');
+    }
+}