Browse Source

Start data only fixtures

These changes rough in the PHPUnit extension, and transaction based
state manager. The state manager will be swappable via the PHPUnit
configuration file. More complex implementations will require
complicated PHPUnit configuration.
Mark Story 5 years ago
parent
commit
fdf487ddf1

+ 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) {

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

@@ -0,0 +1,123 @@
+<?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\Datasource\ConnectionManager;
+
+/**
+ * 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 (method_exists($connection, 'enableSavePoints') && !$connection->enableSavePoints(true)) {
+                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(true);
+            }
+        }
+    }
+
+    /**
+     * Before each test start a transaction.
+     *
+     * @return void
+     */
+    public function beforeTest(): void
+    {
+        $connections = ConnectionManager::configured();
+        foreach ($connections as $connection) {
+            if (strpos($connection, 'test') === false) {
+                continue;
+            }
+            $db = ConnectionManager::get($connection);
+            if (method_exists($db, 'begin')) {
+                $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') === false) {
+                continue;
+            }
+            $db = ConnectionManager::get($connection);
+            if (method_exists($db, 'rollback')) {
+                $db->rollback(true);
+            }
+        }
+    }
+}

+ 37 - 0
src/TestSuite/FixtureSchemaExtension.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace Cake\TestSuite;
+
+use Cake\TestSuite\Fixture\TransactionStrategy;
+
+use PHPUnit\Runner\BeforeTestHook;
+use PHPUnit\Runner\AfterTestHook;
+
+/**
+ * PHPUnit extension to integrate CakePHP's data-only fixtures.
+ */
+class FixtureSchemaExtension implements
+     AfterTestHook,
+     BeforeTestHook
+{
+    protected $state;
+
+    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.
+    }
+
+    public function executeBeforeTest(string $test): void
+    {
+        $this->state->beforeTest();
+    }
+
+    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.');
+    }
+}