Continuous Integration, best practice to input act

2019-02-19 00:45发布

I used Propel ORM to duplicate a table schema, in order to do continuous integration, but Propel only gets me a fully fleshed out schema, it doesn't get me test data (or basic necessary data at all).

How do I get the data from a live/test database with a version controlled propel-gen Propel ORM ecosystem?

1条回答
ゆ 、 Hurt°
2楼-- · 2019-02-19 01:50

They say that "best practice" in anything at all doesn't exist - it's so subjective that one ought to settle for one of several forms of "good practice" instead. I think the below qualifies for that label — and ultimately it works well for me. I've been using PHPUnit for about a year, and perhaps for six months on my projects from scratch.

Here's a synopsis of what I do in the PHPUnit bootstrap phase (specified in phpunit.xml):

  • Drop and create the myproject_test database
  • Call the insert-sql Propel command on a pre-migrations copy of the generated SQL
  • Call the migrate Propel command
  • Scan my test folders for build classes to set up the tests, and run each in turn

The benefit of inserting SQL manually and then running migrations is that migrations get a really thorough testing. This is especially handy since in development I sometimes will do a down, modify a migration class, then do an up to re-run it: it is therefore reassuring to know it will run in order. At present I plan to keep all of my migration history permanently; whilst it will add very minor delay to testing and new builds, upgrade deployments won't be affected.

Since my build depends on having an old SQL file, I avoid using the sql generation command; if it is accidentally issued, the modified SQL files can be trivially reverted in version control.

At present, I am simply using a database name of myproject_test on localhost, so that wherever the tests are run, other database are not affected. It is possible that on build servers you will be required to connect using different credentials: consider detecting the machine name in a switch() statement, and selecting the connection details accordingly.

To give you data to test, I am generally inclined to recommend you don't use an export of data from your live system. There's usually too much of it, for one, and also you generally want to create pieces of data per test, so that tests are completely isolated. I think this is a good idea for two reasons:

  • You can parallelise tests that are independent. So, when your browser test suite takes five hours to run (!) you can set up more build servers to get a green build more quickly.
  • You may wish to locally run a test suite on its own, or a test on its own, or a set of tests matching a certain string, and this may not work if one test depends on another.

This is where my builder classes come in. I use this in my bootstrap.php and call it on each folder containing test classes:

function runBuilders($buildFolder, $namespace)
{
    // I use ! to mark common builders that need to be run first.
    // Since this confuses autoloader, I load that manually.
    $commonBuilder = $buildFolder . '/!CommonBuild.php';
    if (file_exists($commonBuilder))
    {
        require_once $commonBuilder;
    }

    foreach(glob($buildFolder . '/*Build.php') as $class)
    {
        $matches = array();
        $found = preg_match('#/([!a-zA-Z]+)\.php#', $class, $matches);
        if ($found)
        {
            echo '.';

            // Don't use ! characters when creating the class
            $className = str_replace('!', '', $matches[1]);
            call_user_func($namespace . "\\{$className}::build");
        }
    }
}

In !CommonBuild.php I add read-only data that won't be modified by tests, and so it is safe to have just one copy.

I have one build class per PHPUnit test class: for every *Test.php file I have, I will have a corresponding *Build.php. In each builder, a build static method is called, and in that I manually run a method for each test that needs something built. Here is a simple one:

public static function build()
{
    self::buildWriteVarToFieldSuccessfully();
    self::buildWriteVarToFieldUsingFailedMatch();
    self::buildWriteVarToFieldUsingFoundMatch();
    self::buildFailIfVariableIsAnArray();
}

At some point in the future I'll probably use Reflection to run these automatically, like PHPUnit does for tests, but it is fine for now.

Now, in my bootstrap script I fully initialise Propel, using the test connection, so ordinary Propel statements are available. I will thus create just the data I need, like so:

protected static function buildWriteVarToFieldUsingFoundMatch()
{
    // Save an item in the holding table
    $employer = self::createEmployer();
    $job = new \Job\Model\JobHolding();
    $job->setReference('12345');
    $job->setLocationAlias('Rhubarb patch');
    $job->setEmployerId($employer->getPrimaryKey());
    $job->save();

    $process = self::createProcessingUsingRowMatching($employer);
    $process->createSource('VarToFieldTest_buildWriteVarToFieldUsingFoundMatch');
}

I have a naming convention that a test of testWriteVarToFieldUsingFoundMatch in a test class gets a builder called buildWriteVarToFieldUsingFoundMatch in the corresponding build class. It's not enforced as such in code, but this naming helps find one given the other easily (I will often edit both at the same time, using my IDE's split screen feature).

So, in the example above, I only needed one employer record, one job record, one process record and one source record to run this particular test (and not a whole live export). The source record is given a unique name relating to the test name, so that it will only be used in this test (I've found I have to watch out for copy-and-paste errors here - it is quite easy to use the wrong data in a test!).

Creating test data of this kind is quite easy, whatever kind of database you have: user.name fields, address.line1 fields and so forth can usually be created containing a unique identifier, so that when you modify this data in a test, you know that only that test is going to use it, and thus that it is isolated from other tests.

I've opted to run all builders in the bootstrap regardless of what tests are being run, for reasons of simplicity. Since this takes only 15 extra seconds, in my case it's probably not worth doing something more complicated. However, if you wish, you could do something clever with the setUp method in each PHPUnit test, detect the current test (if possible) and then run the appropriate build class.

查看更多
登录 后发表回答