Thursday, December 10, 2015

Testing in Yii 2.0 with Codeception - Fixture Data

I'll admit it -- I'm a phpunit addict. Switching to codeception has been a bit of a struggle for me, just because it feels like it should be intuitive, but it's different enough from what I'm used to that it's taken me a few days to really wrap my head around it. My biggest struggle? Trying to get fixture data to automatically load and unload when functional and acceptance tests are run. I finally dug through the yii2-app-advanced code and discovered the FixtureHelper!! The FixtureHelper is the missing link in getting your fixture data to automagically load and unload when the functional and acceptance tests are run.



Key differences when using CodeCeption with Yii 2.0 vs PHPUnit with Yii 1.1.*

  • Fixture data is first defined by an ActiveFixture class, which is loaded into the fixtures or global fixtures array
  • Fixtures can be loaded (and generated) via the yii command
  • FixtureHelper (for functional/acceptance tests) and DBTestCase (for unit tests) don't come pre-packaged with the basic app (at least, not at this time).
  • Setting up fixture data for multiple schemas is as simple as changing the db setting on the ActiveFixture class that you've extended for your fixtures
  • No "Test" classes for Functional testing, those are all replaced with the "Cept" classes. Unit tests however, keep their old familiar look

NOTE: All of the following assumes when using the command prompt that you are in your @app/tests directory as base, unless otherwise noted.

Setting up your Fixture Data

If you have an active record class that you wish to add fixture data for, you need to define an ActiveFixture class for it, then create a data file that contains the fixture information. Optionally, you could create a generator template which shows how a single item would be constructed, and then use the generator to create the larger fixture data file for you. We'll assume the file is created by hand for now, and go into fixture generators at a later time.

Step 1 - Check your configuration

The information for configuring your test environment should be in the @app/tests/codeception/config/config.php file. In there, the fixture command is added to the controller map, giving you command line access to the fixture models and processes. It should look something like this:
'controllerMap' => [
  fixture' => [
    'class' => 'yii\faker\FixtureController',
    'fixtureDataPath' => '@tests/codeception/fixtures',
    'templatePath' => '@tests/codeception/templates',
    'namespace' => 'tests\codeception\fixtures',
  ],
]
If you're adding fixtures from an external library, you can define those there as well:
'controllerMap' => [
   ...
  'fixtureShared'=>[
    'class'=>'yii\faker\FixtureController',
    'fixtureDataPath'=>'@sharedLib/myObj/tests/fixtures/data',
    'templatePath'=>'@sharedLib/myObj/tests/templates/fixtures',
    'namespace'=>'sharedLib\myObj\tests\fixtures'
  ],
],

You can verify that it's configured and see the available fixture options, by issuing the following command at the prompt from @app/tests:
> codeception/bin/yii 

The two most important commands to learn with that are the load and unload all fixtures.
> codeception/bin/yii fixture/load '*'
> codeception/bin/yii fixture/unload '*'

If you try to use any of the other fixture commands at that point, you'll find that there are no fixtures and no templates available. So lets fix that!

Step 2 - Create the ActiveFixture Class

The file structure is very simple. If you create your fixture class files in the default location of @app/tests/codeception/fixtures, it should look something like this:
namespace tests\codeception\fixtures;

use yii\test\ActiveFixture;

class MyObjectFixture extends ActiveFixture
{
  public $modelClass = 'app\models\MyObject';
}

If you're not doing anything fancy, that's really all you need. It will look for the fixture data file in the same folder the fixture class is in, under the subfolder 'data' and identified by the table name that the Active Record Object uses. If you've got multiple database schemas defined, or want to relocate those data files, you can do so easily by changing your fixture file as shown:
namespace tests\codeception\fixtures;

use yii\test\ActiveFixture;

class MyObjectFixture extends ActiveFixture
{
  public $modelClass = '\sharedLib\myObj\models\MyObject';
  // Assumes you have defined a database connection component keyed as 'altDb'
  public $db = 'altDb'; 
  public $dataFile = '@sharedLib/myObj/fixtures/data/my_objects.php';
}

In the last example, we're assuming that we've switched the fixture to be something we use from a shared library, which we may use in multiple applications accessing the same data backend. (In that case, I'd define the fixture there as well, but now we're really making things complicated -- you get the idea)).

NOTE: When you're setting up the config for codeception, make SURE you're changing the schema configurations to point to the test database, instead of the primary database. The key should stay the same, (i.e., 'db' is still 'db', but the test version should point to myschema_test instead of myschema.

Step 3 - Fixture Data files

The fixture data file stored at either @app/tests/codeception/fixtures/data/ or @sharedLib/myObj/fixtures/data/ depending on which of the above examples you're looking at, is the same as the Yii 1.1.* version:
return [
  'obj1'=>['first_name'=>'Cool','last_name'=>'Beans'],
  'obj2'=>['first_name'=>'Cooler','last_name'=>'Beans'],
];

If I wanted to create a generator template for that, my template file would be found at @app/tests/codeception/templates/fixtures/
/**
 * @var $faker \Faker\Generator
 * @var $index integer
 */
return [
  'first_name'=>$faker->firstName,
  'last_name'=>$faker->lastName
];

Ok, so now what? Well, now that there's fixture data defined, you can go back and use those fixture/load '*' commands to get the data into the database. But, that will just load it once. If you're loading data that is going to be altered with your tests, you're going to want to set them up so that they automatically load and unload at need.

Step 4 - Auto Loading and Unloading Fixtures for Unit Tests

The existing unit tests in the basic application extend the core \yii\codeception\DbTestCase. We're going to change that and create a base DbTestCase for our application, which will establish all the things which we want to load by default on all Unit tests. If the items are not supposed to be global, then they shouldn't go in the base class, so keep that in mind.

Once we've created our DbTestCase, we simply change the inheritance on the UserTest to use tests\codeception\unit\DbTestCase instead of yii\codeception\DbTestCase

At it's most basic, our DbTestCase class should look something like:
namespace tests\codeception\unit;

use yii\test\InitDbFixture;

class DbTestCase extends \yii\codeception\DbTestCase
{
  public function fixtures()
  {
    return [];
  }

  public function globalFixtures()
  {
    return [
      InitDbFixture::className(),
    ];
  }
}
This doesn't give us a whole lot of added value however, as we haven't told it to include any of our defined fixtures. Lets assume we want the MyObjectFixture to load for every test and update the base DbTestCase as follows:
namespace tests\codeception\unit;

use yii\test\InitDbFixture;
use tests\codeception\fixtures\MyObjFixture;

class DbTestCase extends \yii\codeception\DbTestCase
{
  public function fixtures()
  {
    return [
      'objs'=>MyObjFixture::className(),
    ];
  }

  public function globalFixtures()
  {
    return [
      InitDbFixture::className(),
    ];
  }
}

Now, with every Unit tests that extends from DbTestCase, the MyObjFixture data will be loaded in the database cleanly and the 'objs' fixtures will be available via the UnitTest.

Eeexcellent … but that doesn't help us with the functional/acceptance tests yet. So, lets get those.

Step 5 - Auto Loading and Unloading Fixtures for Functional and Acceptance Tests

For this, you need to implement your FixtureHelper for codeception.

This class will be added in the @app/tests/codeception/_support/ folder, and should look something like the following:
namespace tests\codeception\_support;

use Codeception\Module;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
use tests\codeception\fixtures\MyObjFixture;

class FixtureHelper extends Module
{
  use FixtureTrait {
    loadFixtures as public;
    fixtures as public;
    globalFixtures as public;
    createFixtures as public;
    unloadFixtures as protected;
    getFixtures as protected;
    getFixture as protected;
  }
  /**
   * Method called before any suite tests run. Loads User fixture login user
   * to use in acceptance and functional tests.
   * @param array $settings
   */
  public function _beforeSuite($settings = [])
  {
    $this->loadFixtures();
  }
  /**
   * Method is called after all suite tests run
   */
  public function _afterSuite()
  {
    $this->unloadFixtures();
  }
  /**
   * @inheritdoc
   */
  public function fixtures()
  {
    return [
      // Add your fixtures here
      'objs'=>['class'=>MyObjFixture::className()],  // access via key name
      ['class'=>MyOtherObjFixture::className()],     // no access via key name 
    ];
  }
  /**
   * @inheritdoc
   */
  public function globalFixtures()
  {
    return [
      InitDbFixture::className(),
    ];
  }
}
NOTE: If you used shared libraries, I suggest putting everything but the fixtures function into a base class in the shared library, which you can then extend on a per-application basis. That way you're not duplicating all the rest every time.

Once you've defined your helper, you'll need to adjust the .yml files for codeception.

In codeception.yml, ensure the helpers: line is defined in the paths: section
paths:
  test: codeception
  log: codeception/_output
  data: codeception/_data
  helpers: codeception/_support
In functional.suite.yml, ensure the modules: enabled includes your new helper:
modules:
  enabled:
    - Filesystem
    - Yii2
    - tests\codeception\_support\FixtureHelper
The acceptance.suite.yml is similiar:
modules:
  enabled:
    - PhpBrowser
    - tests\codeception\_support\FixtureHelper

Once all of that is done, you need to rebuild your codeception Testers:
> codecept build

Now, you're all set to run your tests, with all your fixture data included!
> codecept run


Final Thoughts

I really love the flexibility of the class based system when working with large code libraries. I have been able to put module specific tests into their own namespace and then pull everything together within a site so that it can have all the appropriate fixtures loaded and be tested within that site without having to duplicate anything that is core to the component. To achieve the same thing in the Yii 1.1 / PHPUnit version, I had to do some complicated juggling to change the fixture data paths and then set them back to the originals when the fixtures were loaded. MUCH simpler now.

I meant to get into more of how to use the testing, but that will have to be a second post!!

Additional Reading

If you want to know more, I suggest the following reading materials: