24
votes

I am trying to implement a Django like test utility for a php application using PHPUnit. By Django like, I mean a separate test db is created from the main database before running the first test and it's dropped after running the last test. The test db needs to be created only once even if many test cases are run at a time.

For this, I took the following approach -

I defined a custom test suite class so that I can write the code for creating and dropping the db in it's setup and teardown methods and then use this class to run the tests as follows

$ phpunit MyTestSuite

MyTestSuite defines a static method named suite where I just use glob and add tests to the testsuite as follows

public static function suite() {
    $suite = new MyTestSuite();

    foreach (glob('./tests/*Test.php') as $tc) {
        require_once $tc;
        $suite->addTestSuite(basename($tc, '.php'));
    }

    return $suite;
}

All Test Case classes extend from a subclass of PHPUnit_Framework_TestCase and the setup and teardown methods of this class take care of loading and clearing initial data from json fixture files.

Now as the no. of tests are increasing, I need to run only a selected tests at a time. But since I am already loading tests using a test suite, the --filter option cannot be used. This makes me feel that this approach may not have been the correct one.

So my question is, what is the correct approach to do something before running the first test and after running the last test irrespective of how PHPUnit finds them ?

PS: I am not using PHPUnit_Extensions_Database_TestCase but my own implementation of creating, populating and dropping the db.

3
For single class you could use setUpBeforeClass and tearDownAfterClass respectively... But for your case. The whole approach looks like it violates one of the major requirements for unit tests: isolation. All the tests should be isolated from each other, and it implies loading fixtures before each test casezerkms
I have mentioned that fixtures are loaded and cleared before and after each test case. But before running the test suite, the actual db the app uses is copied to a test db first and all it's tables are truncated. The code that's tested affects this db. After all tests have been run, it's dropped.naiquevin

3 Answers

36
votes

I recently encountered something where I needed to solve the very same issue. I tried Edorian's answer with the __destruct method of a custom class, but it seemed to be run at the end of every test rather than at the conclusion of all tests.

Instead of using a special class in my bootstrap.php file, I utilized PHP's register_shutdown_function function to handle database cleanup after the conclusion of all my tests, and it seemed to work perfectly.

Here's an example of what I had in my bootstrap.php file

register_shutdown_function(function(){
   some_db_cleanup_methods();
});
18
votes

My two spontaneous ideas that don't use "Test Suites". One that does is at the bottom.

Test Listener

Using PHPUnits test listeners you could do a

  public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
  {
       if($suite->getName() == "yourDBTests") { // set up db
  }

  public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
  {
       if($suite->getName() == "yourDBTests") { // tear down db
  }

You can define all your DB tests in a testsuite in the xml configuration file like shown in the docs

<phpunit>
  <testsuites>
    <testsuite name="db">
      <dir>/tests/db/</dir>
    </testsuite>
    <testsuite name="unit">
      <dir>/tests/unit/</dir>
    </testsuite>
  </testsuites>
</phpunit>

Bootstrap

Using phpunits bootstrap file you could create a class that creates the DB and tears it down in it's own __destruct method when the process ends.

Putting the reference to the object in some global scope would ensure the object only gets destructured at the end off all tests. ( As @beanland pointed out: Using register_shutdown_function() makes a whole lot more sense!)


Using Test suites:

http://www.phpunit.de/manual/3.2/en/organizing-test-suites.html shows:

<?php

class MySuite extends PHPUnit_Framework_TestSuite
{
    public static function suite()
    {
        return new MySuite('MyTest');
    }

    protected function setUp()
    {
        print "\nMySuite::setUp()";
    }

    protected function tearDown()
    {
        print "\nMySuite::tearDown()";
    }
}

class MyTest extends PHPUnit_Framework_TestCase
{
    public function testWorks() {
        $this->assertTrue(true);
    }
}

this works well in PHPUnit 3.6 and will work in 3.7. It's not in the current docs as the "Test suite classes" are somewhat deprecated/discouraged but they are going to be around for quite some time.


Note that tearing down and setting up the whole db for each test case can be quite useful to fight inter-test-dependencies but if you don't run the tests in memory (like sqlite memory) the speed might not be worth it.

3
votes

In 2020 year @edorian's way is already deprecated:

/**
 * @throws Exception
 *
 * @deprecated see https://github.com/sebastianbergmann/phpunit/issues/4039
 */
public function testSuiteLoaderClass(): string
{
    ///
}

Source

New solution

Now we need to use the TestRunner via extensions. Add this code in phpunit.xml:

<extensions>
    <extension class="Tests\Extensions\Boot"/>
</extensions>
<testsuites>
  ...
</testsuites>

Tests/Extensions/Boot.php:

<?php

namespace Tests\Extensions;

use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\BeforeFirstTestHook;

class Boot implements BeforeFirstTestHook, AfterLastTestHook
{
    public function executeBeforeFirstTest(): void
    {
        // phpunit --testsuite Unit
        echo sprintf("testsuite: %s\n", $this->getPhpUnitParam("testsuite"));

        // phpunit --filter CreateCompanyTest
        echo sprintf("filter: %s\n", $this->getPhpUnitParam("filter"));

        echo "TODO: Implement executeBeforeFirstTest() method.";
    }

    public function executeAfterLastTest(): void
    {
        // TODO: Implement executeAfterLastTest() method.
    }

    /**
     * @return string|null
     */
    protected function getPhpUnitParam(string $paramName): ?string
    {
        global $argv;
        $k = array_search("--$paramName", $argv);
        if (!$k) return null;
        return $argv[$k + 1];
    }
}

Execution

Pure php:

phpunit --testsuite Unit --filter CreateCompanyTest

Laravel:

php artisan test --testsuite Unit --filter CreateCompanyTest

output:

PHPUnit 9.3.10 by Sebastian Bergmann and contributors.

testsuite: Unit
filter: CreateCompanyTest
TODO: Implement executeBeforeFirstTest() method.