Skip to content

unit testing

Harmen Janssen edited this page Mar 20, 2018 · 2 revisions

Unit testing

We strive to have a very complete test suite to provide developers a way to validate changes and new functionality. Garp uses PHPUnit, albeit an older version, to execute unit tests.

Installation

See http://code.grrr.nl/golem/wiki/installing-php-unit for more!

Running the suite

Just run phpunit in the root of your project!

phpunit

Test cases can be grouped (see Grouping test cases below). This allows you to run only the given group:

phpunit --group=MyGroup

Database

Make sure you have a database setup for your testing environment! It can be configured just like you would the development database in /application/configs/db.ini. You might want to run a spawn beforehand:

g spawn --e=testing

Depending on your app, you might also need snippets:

g snippet create --e=testing

Writing tests

Conventionally, the name of a testcase follows the name of the class it tests.

A test for /application/modules/default/models/Article.php should go in /tests/application/modules/default/models/ArticleTest.php and look a little something like this:

class Model_ArticleTest extends Garp_Test_PHPUnit_TestCase {

}

Inside the test class, all methods starting with test are executed by phpunit:

class Model_ArticleTest extends Garp_Test_PHPUnit_TestCase {
  
  public function testShouldOnlyFetchPublishedArticles() {
    $model = new Model_Article();
    $this->assertCount(3, $model->fetchAll());
  }

}

Alternatively, add @test to the docblock for the test method (might aid readability, depending on your preference):

class Model_ArticleTest extends Garp_Test_PHPUnit_TestCase {
  
  /** @test */
  public function should_only_fetch_published_articles() {
    $model = new Model_Article();
    $this->assertCount(3, $model->fetchAll());
  }

}

A couple of things to take from this:

  • It's encouraged to make the method name self-documenting. Try to make it into a little sentence that communicates the intent of the test. We like underscores over camelcase for this very purpose.
  • PHPUnit offers a wide range of assertSomething() methods you can use to do assertions. Here is a complete list.

Testing controllers

⚠️ Testing controllers is deprecated because Zend Framework's ControllerTestCase depended on old phpunit versions._

setUp and tearDown

Two special methods can be used to prepare your environment: setUp and tearDown. The former is called before every test, the latter after every test. Use them to prepare your environment and cleanup after yourself.

It's important enough to write in all caps that YOU MUST ALWAYS CALL parent::setUp() AND parent::tearDown()! If you don't you're in the best case missing out on very, very useful functionality, and in the worst case leaving behind some debris from your testing:

public function setUp() {
  parent::setUp();
  // setup some stuff
}

public function tearDown() {
  parent::tearDown();
  // cleanup after yourself
}

The following is an example of what automatically gets done for you in Garp's setUp and tearDown methods:

  • Clearing the cache
  • Inserting and deleting mock data (see below)
  • Making sure Garp_Auth is setup
  • Making sure no user is logged in

All Garp_Test_PHPUnit_TestCase and Garp_Test_PHPUnit_ControllerTestCase instances are setup with a _helper property, thru which various utility functions can be accessed. The following two chapters elaborate on this.

Configuring the runtime

The helper allows you to set configuration values at runtime. This is especially helpful because the unit test environment should be predictable. You don't want your Garp tests to fail because the application at hand happens to be configured differently.

// From a testcase method
$this->_helper->injectConfigValues(array(
  'cdn' => array('domain' => 'http://example.com')
));

As you can see, the parameter follows the usual ini file structure. Everything you set in an ini file can also be set by the helper.

Note, the helper merges the given configuration values with the configuration runtime, so only the values you pass to the helper are changed.

Logging in users

Need a logged in user for a test?

$this->_helper->login(array('name' => 'Henk', 'id' => 5, 'role' => 'fire marshall'));

Inserting mock data

Two possible approaches:

Insert mock data at runtime from a single test

$postId = $this->_helper->insertMockData(
  new Model_BlogPost,
  ['status' => 'PUBLISHED']
);

Note that the second argument is optional and can be used to override randomly generated data.

Note that you should configure a database profiler to make sure inserted data gets automatically removed

[testing : development]

resources.db.params.profiler.enabled = true

Specify mock data as property of the test class

Note that the previous method is more preferable and there are not a lot of reasons to specify mockdata globally unless you need the same data for every test.

Specify mock data as a property of the test class:

$_mockData = array(
  'Tag' => array(
    array('name' => 'development', 'id' => 1),
    array('name' => 'design', 'id' => 2),
    array('name' => 'project management', 'id' => 3)
  ),
  'Article' => array(
    array('name' => 'How to Javascript in 1 hour', 'id' => 1),
    array('name' => 'Five CSS properties you might not know!', 'id' => 2)
  ),
  'ArticleTag' => array(
    array('article_id' => 1, 'tag_id' => 1),
    array('article_id' => 2, 'tag_id' => 1),
    array('article_id' => 2, 'tag_id' => 2)
  )
);

This will insert the given records automatically before every test and deletes them after each test. It's also important to note that the $_mockData array will be populated by fresh data from the database, for you to use in the tests.

For instance, in the case of the above example, $this->_mockData['Tag'][0] will most likely be populated with:

['name' => 'development', 'id' => 1, 'created' => '2014-11-11 13:03', 'modified' => null, 'slug' => 'development']

For internationalized models, specify 'i18n' => true on the array:

$_mockData = [
  'Tag' => [
    'i18n' => true,
    ['name' => ['en' => 'development'], 'id' => 1],
    ['name' => ['en' => 'design'], 'id' => 2],
    ['name' => ['en' => 'project management'], 'id' => 3]
  ]
];

Grouping test cases

Running the whole test suite can take a lot of time. Grouping your cases allows you to run a smaller portion and thus speed up development. Add a group like so:

/**
 * @group Foobar
 */
class FoobarTest extends Garp_Test_PHPUnit_TestCase {
}

You can then run only that group by running

phpunit --group=Foobar

(this will run the group Foobar on both the Garp and default modules. You can add --module=default to filter further)

Using Mockery

Mockery is a great mock framework that allows you to mock services you'd rather not call during testing.

A quick example:

$mockNotifier = \Mockery::mock('App_Notifier');
$mockNotifier->shouldReceive('sendNotification')
    ->with(
        'bestuur_afgehandeld',
        \Mockery::any(),
        $expectedMutation
    )
    ->once();

$statusUpdate = new App_Aanvraag_StatusUpdate('vloa', $aanvraag1, $mockNotifier);
$statusUpdate->save(['beschikker_status' => Status::BESCHIKKER_TOEGEKEND], $userId);

This tests wether method sendNotification of class App_Notifier as dependency of App_Aanvraag_StatusUpdate gets called with three specific arguments.

Check out this page for full documentation on how to integrate with PHPUnit.

Testing workflow

For starters, find articles on Test Driven Development for a broader overview.

In a nutshell, you should:

  • Create the TestCase class for the given class
  • Think about the environment necessary for running the test
  • Use the helper or setUp to setup said environment
  • Start adding tests for the functionality that should be in the class. Write the test first, then the implementation, which should be doing minimum effort to make the test succeed.
  • Run the test after every modification. This means you're starting with a failing test, then succeeding, then failing again, and so on and so forth.
  • Expand the TestCase and the functionality it tests until it fulfils your business goal
  • When you're done, run the entire suite one more time to ensure your work did not break other stuff
Clone this wiki locally