Customizing Codeception Database Cleanup

Recently, I was looking into ways to speed up the runtime of the test suite at Blopboard. We use the Codeception framework to write functional tests for our REST API, part of which entails putting the database into a known state using Codeception’s Db module. The behavior of this module is similar to that of the PHPUnit Database extension with one exception: where PHPUnit only truncates tables and leaves their schemas intact, Codeception removes the database structure and expects the SQL dump it uses to recreate it between tests.

I must admit to not understanding this design decision of Codeception, nor attempts to clarify it. Be that as it may, I had a hunch that subverting it might lead to a faster runtime for our test suite, so I set about trying to find a solution to facilitate that. I found one, and while it’s a bit hacky, it works.

<?php

namespace Codeception\Module;

/**
 * Extends the standard Db helper to override cleanup behavior so that tables
 * are truncated rather than dropped and recreated between tests.
 */
class DbHelper extends \Codeception\Module\Db
{
	protected function cleanup()
	{
		$dbh = $this->driver->getDbh();
		if (! $dbh) {
			throw new ModuleConfigException(
				__CLASS__,
				"No connection to database. Remove this module from config if you don't need database repopulation"
			);
		}

		try {
			if (! count($this->sql)) {
				return;
			}

			/** Start **/
			$dbh->exec('SET FOREIGN_KEY_CHECKS=0;');
			$res = $dbh->query("SHOW FULL TABLES WHERE TABLE_TYPE LIKE '%TABLE';")->fetchAll();
			foreach ($res as $row) {
				$dbh->exec('TRUNCATE TABLE `' . $row[0] . '`');
			}
			$dbh->exec('SET FOREIGN_KEY_CHECKS=1;');
			/** End **/

		} catch (\Exception $e) {
			throw new ModuleException(__CLASS__, $e->getMessage());
		}
	}
}

The above module class is used in place of the Db module. To come up with it, I started by digging into the logic of the Db module class itself. Codeception has several hook methods for modules that it calls internally. One of these is _initialize(), which is called after the module class is instantiated and configuration for it is loaded but before any tests are run.

Looking at the _initialize() implementation in the Db module class, I found that it makes a call to a method to obtain a driver object for the particular database in use. This driver object implements a cleanup() method that the Db module class’s own cleanup() method calls between tests to handle resetting the database state.

There’s a problem here, though: the call to obtain the driver object is to a static method, which means there’s no way for me to specify my own logic for how to obtain a driver object rather than the logic that Codeception uses by default. This inhibits extensibility as well as testability.

I could have gotten around this by extending the Db module class and overriding its _initialize() method to call out to different code to obtain an instance of my own driver class. However, that would have meant duplicating most of the logic of that method, which is not of a trivial size. This would raise the likelihood that my code would not work with subsequent versions of Codeception if the method I was overriding changed.

In the end, the alternative I found was to instead extend the Db module class and override its cleanup() method. While this still results in duplication of code, the code being duplicated (which is demarcated by /** Start **/ and /** End **/ comments in the above code sample) is shorter, simpler, and less likely to be changed such that it impacts my code’s functionality. It is worth noting, however, that the above code sample will likely only work with MySQL, and would need modifications to work with other database servers.

Had the Db module class encapsulated its call to Driver::create() within an instance method, I could have simply overridden that method in my subclass and had a cleaner solution.

Alternatively, Codeception could have supported a solution like this:

<?php

namespace Codeception\Module\Db;

interface DriverFactoryInterface
{
  public function create($dsn, $user, $password);

  // ...
}

class DriverFactory implements DriverFactoryInterface
{
  public function create($dsn, $user, $password)
  {
    // The contents of Driver::create() would go here.
  }
}

namespace Codeception\Module;

class Db extends \Codeception\Module
{
  protected $driverFactory;
  protected $driver;

  public function _initialize()
  {
    // ...
    if (!isset($this->config['driverFactoryClass'])) {
      $this->config['driverFactoryClass'] = '\Codeception\Module\Db\DriverFactory';
    }
    $this->driver = $this->getDriverFactory()->create(
      $this->config['dsn'],
      $this->config['user'],
      $this->config['password']
    );
    // ...
  }

  public function getDriverFactory()
  {
    if (!$this->driverFactory) {
      $driverFactoryClass = $this->config['driverFactoryClass'];
      $this->setDriverFactory(new $driverFactoryClass);
    }
    return $this->driverFactory;
  }

  public function setDriverFactory(DriverFactoryInterface $driverFactory)
  {
    $this->driverFactory = $driverFactory;
  }

  // ...
}

In the above solution, there is a DriverFactoryInterface interface with, among others, a create() instance method, and a DriverFactory class that implements this interface. The Db module class allows the specification of a class that implements this interface via its configuration. It then handles instantiating this class and calls that object’s create() method from its _initialize() method rather than calling Driver::create() as it presently does. With this code in place, I could write my own class implementing the interface to return my own driver. This would allow me to accomplish my goal without having to resort to subclassing.

In any case, my hunch and solution paid off: with the solution in place, we were able to cut our test suite runtime by roughly 30%. Another pleasant side effect was that I no longer needed to maintain a copy of our database schema apart from the one we already maintain using Liquibase.

I hope this solution and my thoughts on Codeception’s present design are helpful to someone. Thanks for reading.

2 Comments

  1. Parham Doustdar says:

    Hi,

    Thanks for the article. I just came up with this problem yesterday. Since I’m used to using Yii and PHPUnit, it seemed a very strange solution to the problem to me. I’m glad someone else shares this opinion and has found a solution for it!

  2. Ben says:

    Thanks for the article.

    I had a problem with testing authentication using Doctrine in Codeception. I configured it to use ‘cleanup’ but it would rollback the transaction after the authentication which meant that it would bounce back to the login page after successful authentication. I got round it by adding the following in my Cept file before and after the test steps:

    $entityManager = \Codeception\Module\Doctrine2::$em;

    $connection = $entityManager->getConnection();

    $connection->exec(‘SET FOREIGN_KEY_CHECKS=0;’);
    $connection->exec( ‘TRUNCATE TABLE users’ );
    $connection->exec( ‘ALTER TABLE users AUTO_INCREMENT = 1;’ );
    $connection->exec(‘SET FOREIGN_KEY_CHECKS=1;’);

    To truncate all the tables, do the following:

    $connection->exec(‘SET FOREIGN_KEY_CHECKS=0;’);

    $rows = $connection->fetchAll( “SHOW FULL TABLES WHERE TABLE_TYPE LIKE ‘%TABLE’;”);;

    foreach ($rows[0] as $row) {
    if($row == ‘BASE TABLE’){
    continue;
    }
    $connection->exec(‘TRUNCATE TABLE `’ . $row . ‘`’);
    $connection->exec(‘ALTER TABLE `’ . $row . ‘`’ . ‘ AUTO_INCREMENT = 1;’);
    }

    $connection->exec(‘SET FOREIGN_KEY_CHECKS=1;’);

Leave a Reply