3

I'm following civix's upgrade instructions to enable unit tests to be executed with standalone phpunit.

I've got my code in a work-in-progress git branch. The first commit captures the civix-generated boilerplate phpunit.xml.dist and bootstrap.php. The second commit captures my attempts to adapt an existing test to the new way of doing things.

With these changes in place, I do:

$ cd /path/to/webroot/sites/all/modules/civicrm/tools/extensions/civivolunteer
$ phpunit4 tests/phpunit/api/v3/VolunteerProjectTest.php

... and get:

PHP Fatal error:  Class 'VolunteerTestAbstract' not found in /path/to/webroot/sites/all/modules/civicrm/tools/extensions/civivolunteer/tests/phpunit/api/v3/VolunteerProjectTest.php on line 6
PHP Stack trace:
PHP   1. {main}() /usr/local/lib/civicrm-buildkit/bin/phpunit4:0
PHP   2. PHPUnit_TextUI_Command::main() /usr/local/lib/civicrm-buildkit/bin/phpunit4:545
PHP   3. PHPUnit_TextUI_Command->run() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/TextUI/Command.php:100
PHP   4. PHPUnit_Runner_BaseTestRunner->getTest() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/TextUI/Command.php:123
PHP   5. PHPUnit_Runner_BaseTestRunner->loadSuiteClass() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/Runner/BaseTestRunner.php:67
PHP   6. PHPUnit_Runner_StandardTestSuiteLoader->load() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/Runner/BaseTestRunner.php:123
PHP   7. PHPUnit_Util_Fileloader::checkAndLoad() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/Runner/StandardTestSuiteLoader.php:39
PHP   8. PHPUnit_Util_Fileloader::load() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/Util/Fileloader.php:38
PHP   9. include_once() phar:///usr/local/lib/civicrm-buildkit/bin/phpunit4/phpunit/Util/Fileloader.php:56

Any suggestions?

2

Civi's autoloader does not load classes outside the CRM directory. In order for that to work, you'll have a few options:

  • Implement your own autoloader capable of loading classes inside the test/phpunit dir
  • Use require to include the class
  • Put the VolunteerTestAbstract class inside your CRM dir

I usually use the second option for that and include/require the class(es) on the bootstrap.php file.

I also like the third option. For that, you would put the class somewhere like CRM_Volunteer_Test_VolunteerTestAbstract and then just extend it. No need for requires or to implement your own autoloader.

  • Thanks, Davi. Have called it a day but will have another look over the weekend. – universalhandle Nov 12 '16 at 2:29
4

This is an issue with intermediate base-classes for tests (like VolunteerTestAbstract). The most reliable solution I've found is to put a require_once to load the intermediate base-class. For example, this is what I used in uk.co.vedaconsulting.mosaico:

require_once __DIR__ . '/TestCase.php';

class CRM_Mosaico_MosaicoTemplateTest extends CRM_Mosaico_TestCase implements EndToEndInterface {
     ...
}

Unfortunately, it's hard to combine PHPUnit with a classloader that understands extensions. To see why, consider an example command:

phpunit4 tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php

Which would then:

  • A. Load test files, e.g.
    1. Load tests/phpunit/bootstrap.php
    2. Load tests/phpunit/CRM/Mosaico/MosaicoTemplateTest.php (which depends on a base class like CRM_Mosaico_TestCase)
    3. Load tests/phpunit/CRM/Mosaico/TestCase.php
  • B. Begin executing the test logic, e.g.
    1. Execute CiviTestListener
    2. Execute setupBeforeClass()
    3. Execute setUp()

The problem arises if the database-initialization (eg \Civi\Test::headless()->installMe(__DIR__)->apply()) is put in phase B test-logic (e.g. CiviTestListener or setupBeforeClass() or setUp()) . The database-initialization determines which extensions are active. The active extensions determine the class-loader. But all of this comes too late. During steps A2=>A3 (when we need autoloading), the extension-enabled class-loader isn't available yet.

Put another way: any code which references an autoloaded extension class has an implicit dependency on the database.

One can resolve this conundrum a couple ways, e.g.

  • In tests/phpunit/bootstrap.php, fully initialize the system. (Ex: In the line eval(cv('php:boot --level=classloader', 'phpcode'));, change the --level to settings or full.) This makes sense if all the tests are end-to-end (e2e).
  • Load classes in a way which does not implicitly depend on the database. The simplest thing is to do a require_once to load the target class. (EDIT: What's a nice place to put the require_once statement? I originally put it in each test class, but on second thought... it would be more elegant to put it in tests/phpunit/boostrap.php.)

(I'm writing this to help me get the problem a bit clearer in mind... hopefully that will eventually lead to a better solution...)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.