Table of Contents
Unit Testing Dokuwiki
Very briefly explained, Unit Testing is writing code to test “units” of other code, so that tests can be re-run at any time to determine whether or not code has been “broken” (vs. manually testing applications, which is lengthy and potentially unreliable). The word “unit” is used to mean anything that provides a clearly defined API, such as a PHP class or function.
DokuWiki's unit tests are located in the _test
directory of a git checkout. They are not included in the regular releases.
We use the PHPUnit test framework for PHP. This page guides you through preparing your test environment, running tests and writing your own tests.
Setup
Download the correct phar file for your PHP version.
cd dokuwiki/_test php fetchphpunit.php
Running Tests
Running the test suite is simple, here's how.
All Tests
Just change to the _test
directory and run phpunit:
cd _test/ php phpunit.phar
Single Test Files
You can run a specific test file by giving it as a parameter:
cd _test php phpunit.phar tests/inc/input.test.php
Grouped Tests
You include or exclude tests by their group:
cd _test # run all tests that require an internet connection php phpunit.phar --group internet # run all tests that don't require an internet connection php phpunit.phar --exclude-group internet
Plugins
Plugins tests are tagged with a group named plugin_<pluginname>
:
cd _test php phpunit.phar --group plugin_extension
Please note that some plugins may require other plugins in order to pass, either as dependencies, or because some integration code is being tested. Check if they have a requirements.txt
file in their folder for the needed plugins.
Writing Tests
PHPUnit makes writing new tests as painless as possible. In general it means writing a fairly simple PHP class and placing it in the right directory with the right file name. Once that's done, the test can immediately be executed as explained above.
For a general overview on how to write unit tests refer to the PHPUnit manual and the existing tests, our DokuWiki specific parts are described below.
Naming Conventions
Every new bit of code should follow PSR-12 code style and be in classes that can be auto-loaded according to PSR-4. Classes following these conventions should be accompanied by tests, following the same naming conventions but use the \dokuwiki\test\
namespace which is autoloaded from the _test/tests/
directory.
In accordance ẃith PHPUnit conventions test classes should end in Test
while non-test classes (eg. mock objects) omit that postfix.
This means a class \dokuwiki\File\PageResolver
should have a test in \dokuwiki\test\File\PageResolverTest
located in _test/tests/File/PageResolverTest.php
.
Each test class need to inherit from [xref>_test/core/DokuWikiTest.php|\DokuWikiTest]]. A test class can have multiple test functions, each prefixed with test
. Inside these functions your assertion can be written.
For legacy, functional code no namespace is used. Tests are located in the sub directories inc
, lib
, conf
etc. to reflect the directory structure of the files the tested functions are located in. Legacy testing uses the .test.php
extension.
Environment
Our testsuite runs a setUpBeforeClass()
method on instantiating each test class which will:
- setup a minimal
data
directory (copied from_test/data
) in the system's TEMP directory - create a custom config directory in the system's TEMP directory
- with default configs
- some overrides from
_test/conf
- rerun DokuWiki initialization
This setup won't load any plug-ins. If you need to enable a plug-in you can override the DokuWikiTest::$pluginsEnabled
array and provide the plugin names to load.
Note: Remember to call parent::setUpBeforeClass() on overwriting the setUpBeforeClass() method |
---|
Additionally the test suite runs a setUp()
method before every test. It does:
- reload the DokuWiki config
- reload the plugin controller and plug-ins
- reload the event handler
- ensure the existence of some files (see init_files())
- ensure DokuWiki paths (see init_path())
- reload global variables like
$_SERVER
Note: Remember to call parent::setUp() on overwriting the setUp() method |
---|
Event though there is no real process isolation between all tests, each test class should have a pretty clean DokuWiki environment to work on. This also includes DokuWiki's autoloader mechanism, so there should be no need to require_once any files yourself.
To clean up and reset the remainder of a test, the methods tearDown()
and tearDownAfterClass()
are available. Remember here also to use the parent methods, parent::tearDown()
and parent::tearDownAfterClass()
for future robustness.
Integration Tests
Unit tests should test minimal code parts (units) whenever possible. But with a complex application like DokuWiki this isn't always possible. This is why our test suite allows to run simple integration tests as well using a fake request.
Using this mechanism a HTTP request is faked and the resulting HTML output can be inspected. This also allows for testing plugin events.
Running a request
The TestRequest class is used to run a request. A request contains three steps:
- Initialization
- Prepare request
- send request
The initialization is done by:
$request = new TestRequest();
Once it's initialized you can prepare the request. The preparation can be register event hooks or setting request variables.
The request variables are set via setter methods and look like:
Variable | Setter method |
---|---|
$_SERVER | $request->setServer() |
$_SESSION | $request->setSession() |
$_POST | $request->setPost() |
$_GET | $request->setGet() |
Finally you have to execute the request. This can be done by calling the $request->execute('someurl')
method. The return value of the execute method is the html content rendered by DokuWiki.
Additionally there are two methods for POST
and GET
calls. They may be shorter then using the setter methods.
// a get request $request->get(array('id' => 'start'), '/doku.php'); // a post request $request->post(array('id' => 'start'), '/doku.php');
The result of a request is a TestResponse object.
Example: Event hooks
As mentioned the requests are not real requests. Every call you make in your test changes the behavior of DokuWiki on the request.
Here this is used to hook a event.
function testHookTriggering() { global $EVENT_HANDLER; $request = new TestRequest(); // initialize the request $hookTriggered = false; // initialize a test variable // register a hook $EVENT_HANDLER->register_hook('TPL_CONTENT_DISPLAY', 'AFTER', null, function() use (&$hookTriggered) { $hookTriggered = true; } ); // do the request $request->execute(); // check the result of our variable $this->assertTrue($hookTriggered, 'Hook was not triggered as expected!'); }
Example: Using php-dom-wrapper after a request
Sometimes you want to inspect the resulting html of a request. To provide a little comfort we use the php-dom-wrapper library.
With this you can operate in a jQuery like style on the returned HTML.
A simple example is:
function testSimpleRun() { // make a request $request = new TestRequest(); $response = $request->execute(); // get the generator name from the meta tag. $generator = $response->queryHTML('meta[name="generator"]')->attr('content'); // check the result $this->assertEquals('DokuWiki', $generator); }
Plugin and Template Tests
Sometime you need unit tests in your extensions1). Here you can use the same classes and methods as used in the DokuWiki core. The plugin wizard has an option to add an example test in your plugin skeleton. Alternatively you can use the dev Plugin to add tests.
Just put your tests under the <extension dir>/_test/
folder. The tests must conform to the naming convention., eg. your test cases must extend the DokuWikiTest class and the file name must end with Test.php
. You should use the namespace \dokuwiki\plugin\<yourplugin>\test\
.
To work, your plugin needs to be enabled during the test. This is done by putting it and all plugins it depends on in the $pluginsEnabled
member.
Plugin tests should declare a group for all their tests named plugin_<pluginname>
to make it easy to run them separately.
If your plugin needs additional files during testing, you can copy them to the test directory in a setUpBeforeClass()
method. Be sure to call the parent first.
Here's a small example:
/** * @group plugin_data * @group plugins */ class helper_plugin_data_test extends DokuWikiTest { protected $pluginsEnabled = array('data', 'sqlite'); public static function setUpBeforeClass(){ parent::setUpBeforeClass(); // copy our own config files to the test directory TestUtils::rcopy(dirname(DOKU_CONF), dirname(__FILE__).'/conf'); } public function testExample() { $this->assertTrue(true,'if this fails your computer is broken'); } }
A plugin's config can simply be changed by writing to the $conf array. The changes can e.g. be done in the test functions themselves or in the function setUp()
. Config changes in function setUpBeforeClass()
will have no effect. Have a look at this example:
function setUp(){ global $conf; parent::setUp(); $conf ['plugin']['creole']['precedence'] = 'Creole'; $conf ['plugin']['creole']['linebreak'] = 'Whitespace'; }
So the correct indexing for plugins is $conf ['plugin']['plugin name']['option name']
.
Continous Integration with Github Actions
Plugin authors are encouraged to have their tests run automatically on Github Actions.The Plugin Wizard and the dev plugin add an appropriate yaml configuration to your plugin when you select the Unit Test option. The needed build environment is provided by the dokuwiki-travis script.
Requirements
If your tests require additional plugins to be installed, provide a requirements.txt
file in your plugin's root directory. See for the details the README in the dokuwiki-travis repository.
Javascript + Frontend-tests
this section needs to be rewritten for Github Actions since Travis is no longer recommended.
It is possible to integrate javascript-tests written in qunit to the automated testing. The basis for this is npm, grunt and phantomjs2).
- .travis.yml
language: php php: - "5.5" - "5.4" - "5.3" env: - DOKUWIKI=master - DOKUWIKI=stable - DOKUWIKI=old-stable before_install: - wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh - npm install install: sh travis.sh script: - cd _test && phpunit --stderr --group plugin_yourplugin - cd ../lib/plugins/yourplugin && grunt
npm needs a package.json
to install the dependencies:
- package.json
{ "name": "yourplugin", "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-qunit": "^0.7.0", "jquery": "^2.1.4", "qunitjs": "^1.18.0" } }
grunt needs a Gruntfile.js
to define the tasks:
- Gruntfile.js
module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), // the package file to use qunit: { all: ['_jstest/*.html'] } }); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.registerTask('default', ['qunit']); };
Finally the qunit html-files have to be adjusted to be able to work with node_modules for automated testing and with online-libraries for browser-testing:
- qunit.test.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>tests for your plugin</title> <link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.18.0.css"> <script src="../node_modules/jquery/dist/jquery.js"></script> <script>window.jQuery || document.write('<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"><\/script>')</script> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="../node_modules/qunitjs/qunit/qunit.js"></script> <script>window.QUnit || document.write('<script src="//code.jquery.com/qunit/qunit-1.18.0.js"><\/script>')</script> <script src="../script/myscript.js"></script> <script src="mytest.tests.js"></script> </body> </html>
See the edittable plugin for an implementation of this approach.