Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Writing PHPUnit tests: Difference between revisions

From MoodleDocs
(Documentation for MDL-72701)
Line 2: Line 2:


Moodle PHPUnit integration is designed to allow easy adding of new tests. At the start of each test the state is automatically reset to fresh new installation (unless explicitly told not to reset).
Moodle PHPUnit integration is designed to allow easy adding of new tests. At the start of each test the state is automatically reset to fresh new installation (unless explicitly told not to reset).
=Namespaces=
=Namespaces=
All the stuff under **/tests directories is [[Coding style#Namespaces_within_.2A.2A.2Ftests_directories|subject to some simple rules]] when using namespaces. They apply to test cases, fixtures, generators and, in general, any class within those directories. Take a look to them! (grand summary = 100% the same rules that are applied to **/classes directories).
All the stuff under **/tests directories is [[Coding style#Namespaces_within_.2A.2A.2Ftests_directories|subject to some simple rules]] when using namespaces. They apply to test cases, fixtures, generators and, in general, any class within those directories. Take a look to them! (grand summary = 100% the same rules that are applied to **/classes directories).
=Testcase classes=
=Testcase classes=
There are three basic test class that are supposed to used in all Moodle unit tests - basic_testcase, advanced_testcase and provider_testcase. '''Please note it is strongly recommended to put only one testcase into each class file.'''
There are three basic test class that are supposed to used in all Moodle unit tests - basic_testcase, advanced_testcase and provider_testcase. '''Please note it is strongly recommended to put only one testcase into each class file.'''
;basic_testcase : Very simple tests that do not modify database, dataroot or any PHP globals. It can be used for example when trying examples from the official PHPUnit tutorial.
;basic_testcase : Very simple tests that do not modify database, dataroot or any PHP globals. It can be used for example when trying examples from the official PHPUnit tutorial.
;advanced_testcase : Enhanced testcase class enhanced for easy testing of Moodle code.
;advanced_testcase : Enhanced testcase class enhanced for easy testing of Moodle code.
;provider_testcase: Enhanced testcase class, enhanced for easy testing of [[Privacy API|Privacy Providers]].
;provider_testcase: Enhanced testcase class, enhanced for easy testing of [[Privacy API|Privacy Providers]].
There is a fourth testcase class that is specially designed for testing of our Moodle database layer, it should not be used for other purposes.
There is a fourth testcase class that is specially designed for testing of our Moodle database layer, it should not be used for other purposes.
== Assertions ==
== Assertions ==
The complete list of assertions can be found in the links below.
The complete list of assertions can be found in the links below.
{| class="wikitable" border="1"
{| class="wikitable" border="1"
|-
|-
Line 41: Line 33:
| PHPUnit 6.5
| PHPUnit 6.5
|[https://phpunit.de/manual/6.5/en/assertions.html Documentation]
|[https://phpunit.de/manual/6.5/en/assertions.html Documentation]
|}
|}
==Sample plugin testcase==
==Sample plugin testcase==
PHPUnit tests are located in <tt>tests/*_test.php</tt> files in your plugin, for example <tt>mod/myplugin/tests/sample_test.php</tt>, the file should contain only one class that extends <tt>advanced_testcase</tt>:
PHPUnit tests are located in <tt>tests/*_test.php</tt> files in your plugin, for example <tt>mod/myplugin/tests/sample_test.php</tt>, the file should contain only one class that extends <tt>advanced_testcase</tt>:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  namespace mod_myplugin;
  namespace mod_myplugin;
Line 57: Line 45:
  }
  }
</syntaxhighlight>
</syntaxhighlight>
See [[PHPUnit integration#Class and file naming rules]] for more information.
See [[PHPUnit integration#Class and file naming rules]] for more information.
==Inclusion of Moodle library files==
==Inclusion of Moodle library files==
If you want to include some Moodle library files you should always declare '''global $CFG'''. The reason is that testcase files may be included from non-moodle code which does not make the global $CFG available automatically.
If you want to include some Moodle library files you should always declare '''global $CFG'''. The reason is that testcase files may be included from non-moodle code which does not make the global $CFG available automatically.
==Automatic state reset==
==Automatic state reset==
By default after each test Moodle database and dataroot is automatically reset to the original state which was present right after installation. make sure to use $this->resetAfterTest() to indicate that the database or changes of standard global variables are expected.
By default after each test Moodle database and dataroot is automatically reset to the original state which was present right after installation. make sure to use $this->resetAfterTest() to indicate that the database or changes of standard global variables are expected.


If you received the error "Warning: unexpected database modification, resetting DB state" it is because the test is not using <tt>$this->resetAfterTest()</tt>.
If you received the error "Warning: unexpected database modification, resetting DB state" it is because the test is not using <tt>$this->resetAfterTest()</tt>.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  namespace mod_myplugin;
  namespace mod_myplugin;
Line 85: Line 68:
  }
  }
</syntaxhighlight>
</syntaxhighlight>
=Generators=
=Generators=
Tests that need to modify default installation may use generators to create new courses, users, etc. All examples on this page should be used from test methods of a test class derived from advanced_testcase.
Tests that need to modify default installation may use generators to create new courses, users, etc. All examples on this page should be used from test methods of a test class derived from advanced_testcase.


Note if you are using PHPUnit [https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers @dataProvider] functions to provide parameters to unit tests, you can not use the data generator or change the user etc in the data provider function. Data providers '''must not instantiate/create data'''. Just define it. And then, the test body can proceed with the instantiation/creation.
Note if you are using PHPUnit [https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers @dataProvider] functions to provide parameters to unit tests, you can not use the data generator or change the user etc in the data provider function. Data providers '''must not instantiate/create data'''. Just define it. And then, the test body can proceed with the instantiation/creation.
==Creating users==
==Creating users==
At the start of each test there are only two users present - guest and administrator. If you need to add more test accounts use:
At the start of each test there are only two users present - guest and administrator. If you need to add more test accounts use:
Line 97: Line 77:
  $user = $this->getDataGenerator()->create_user();
  $user = $this->getDataGenerator()->create_user();
</syntaxhighlight>
</syntaxhighlight>
You may also specify properties of the user account, for example:
You may also specify properties of the user account, for example:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $user1 = $this->getDataGenerator()->create_user(array('email'=>'user1@example.com', 'username'=>'user1'));
  $user1 = $this->getDataGenerator()->create_user(array('email'=>'user1@example.com', 'username'=>'user1'));
</syntaxhighlight>
</syntaxhighlight>
By default no user is logged-in, use setUser() method to change current $USER value:
By default no user is logged-in, use setUser() method to change current $USER value:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $this->setUser($user1);
  $this->setUser($user1);
</syntaxhighlight>
</syntaxhighlight>
Guest and admin accounts have a shortcut methods:
Guest and admin accounts have a shortcut methods:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
Line 113: Line 90:
  $this->setAdminUser();
  $this->setAdminUser();
</syntaxhighlight>
</syntaxhighlight>
Null can be used to set current user back to not-logged-in:
Null can be used to set current user back to not-logged-in:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $this->setUser(null);
  $this->setUser(null);
</syntaxhighlight>
</syntaxhighlight>
==Creating course categories==
==Creating course categories==
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $category1 = $this->getDataGenerator()->create_category();
  $category1 = $this->getDataGenerator()->create_category();
  $category2 = $this->getDataGenerator()->create_category(array('name'=>'Some subcategory', 'parent'=>$category1->id));
  $category2 = $this->getDataGenerator()->create_category(array('name'=>'Some subcategory', 'parent'=>$category1->id));
</syntaxhighlight>
</syntaxhighlight>
==Creating courses==
==Creating courses==
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $course1 = $this->getDataGenerator()->create_course();
  $course1 = $this->getDataGenerator()->create_course();
Line 134: Line 106:
  $course2 = $this->getDataGenerator()->create_course(array('name'=>'Some course', 'category'=>$category->id));
  $course2 = $this->getDataGenerator()->create_course(array('name'=>'Some course', 'category'=>$category->id));
</syntaxhighlight>
</syntaxhighlight>
==Creating activities==
==Creating activities==
Some activity plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php.
Some activity plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php.


Example of creation of new course with one page resource:
Example of creation of new course with one page resource:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $course = $this->getDataGenerator()->create_course();
  $course = $this->getDataGenerator()->create_course();
Line 146: Line 115:
  $generator->create_instance(array('course'=>$course->id));
  $generator->create_instance(array('course'=>$course->id));
</syntaxhighlight>
</syntaxhighlight>
The following is functionally the same, but a bit shorter:
The following is functionally the same, but a bit shorter:
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
Line 152: Line 120:
  $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
  $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
</syntaxhighlight>
</syntaxhighlight>
==Creating cohorts==
==Creating cohorts==
{{Moodle 2.4}}
{{Moodle 2.4}}
Since 2.4 there the data generator supports creation of new cohorts.
Since 2.4 there the data generator supports creation of new cohorts.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
  $cohort = $this->getDataGenerator()->create_cohort();
  $cohort = $this->getDataGenerator()->create_cohort();
</syntaxhighlight>
</syntaxhighlight>
==Simplified user enrolments==
==Simplified user enrolments==
{{Moodle 2.4}}
{{Moodle 2.4}}
Instead of standard enrolment API it is possible to use simplified method in data generator. It is intended to be used with self and manual enrolment plugins.
Instead of standard enrolment API it is possible to use simplified method in data generator. It is intended to be used with self and manual enrolment plugins.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->enrol_user($userid, $courseid);
$this->getDataGenerator()->enrol_user($userid, $courseid);
Line 170: Line 134:
$this->getDataGenerator()->enrol_user($userid, $courseid, $teacherroleid, 'manual');
$this->getDataGenerator()->enrol_user($userid, $courseid, $teacherroleid, 'manual');
</syntaxhighlight>
</syntaxhighlight>
==Creating scales==
==Creating scales==
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_scale();
$this->getDataGenerator()->create_scale();
$this->getDataGenerator()->create_scale(array('name' => $name, 'scale' => $scale, 'courseid' => $courseid, 'userid' => $userid, 'description' => description, 'descriptionformat' => $descriptionformat));
$this->getDataGenerator()->create_scale(array('name' => $name, 'scale' => $scale, 'courseid' => $courseid, 'userid' => $userid, 'description' => description, 'descriptionformat' => $descriptionformat));
</syntaxhighlight>
</syntaxhighlight>
==Creating roles==
==Creating roles==
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_role();
$this->getDataGenerator()->create_role();
$this->getDataGenerator()->create_role(array('shortname' => $shortname, 'name' => $name, 'description' => description, 'archetype' => $archetype));
$this->getDataGenerator()->create_role(array('shortname' => $shortname, 'name' => $name, 'description' => description, 'archetype' => $archetype));
</syntaxhighlight>
</syntaxhighlight>
==Creating tags==
==Creating tags==
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_tag();
$this->getDataGenerator()->create_tag();
Line 198: Line 156:
));
));
</syntaxhighlight>
</syntaxhighlight>
==Groups==
==Groups==
===Creating groups===
===Creating groups===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
Line 206: Line 162:
$this->getDataGenerator()->create_group(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));
$this->getDataGenerator()->create_group(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));
</syntaxhighlight>
</syntaxhighlight>
===Adding users to groups===
===Adding users to groups===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
Line 212: Line 167:
$this->getDataGenerator()->create_group_member(array('userid' => $userid, 'groupid' => $groupid, 'component' => $component, 'itemid' => $itemid));
$this->getDataGenerator()->create_group_member(array('userid' => $userid, 'groupid' => $groupid, 'component' => $component, 'itemid' => $itemid));
</syntaxhighlight>
</syntaxhighlight>
===Creating groupings===
===Creating groupings===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
Line 218: Line 172:
$this->getDataGenerator()->create_grouping(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));
$this->getDataGenerator()->create_grouping(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));
</syntaxhighlight>
</syntaxhighlight>
===Adding groups to groupings===
===Adding groups to groupings===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_grouping_group(array('groupingid' => $groupingid, 'groupid' => $groupid));
$this->getDataGenerator()->create_grouping_group(array('groupingid' => $groupingid, 'groupid' => $groupid));
</syntaxhighlight>
</syntaxhighlight>
==Repositories==
==Repositories==
===Creating repository instances===
===Creating repository instances===
{{Moodle 2.5}}
{{Moodle 2.5}}
Some respository plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php..
Some respository plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php..
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_repository($type, $record, $options);
$this->getDataGenerator()->create_repository($type, $record, $options);
</syntaxhighlight>
</syntaxhighlight>
===Creating repository types===
===Creating repository types===
{{Moodle 2.5}}
{{Moodle 2.5}}
Some respository plugins include type generators. The generator class are defined in plugindirectory/tests/generator/lib.php..
Some respository plugins include type generators. The generator class are defined in plugindirectory/tests/generator/lib.php..
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_repository_type($type, $record, $options);
$this->getDataGenerator()->create_repository_type($type, $record, $options);
</syntaxhighlight>
</syntaxhighlight>
==Creating grades==
==Creating grades==
===Grade categories===
===Grade categories===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid));
$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid));
$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid, 'fullname' => $fullname));
$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid, 'fullname' => $fullname));
</syntaxhighlight>
</syntaxhighlight>
===Grade items===
===Grade items===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_grade_item();
$this->getDataGenerator()->create_grade_item();
$this->getDataGenerator()->create_grade_item(array('itemtype' => $itemtype, 'itemname' => $itemname, 'outcomeid' => $outcomeid, 'scaleid' => $scaleid, 'gradetype' => $gradetype));
$this->getDataGenerator()->create_grade_item(array('itemtype' => $itemtype, 'itemname' => $itemname, 'outcomeid' => $outcomeid, 'scaleid' => $scaleid, 'gradetype' => $gradetype));
</syntaxhighlight>
</syntaxhighlight>
===Outcomes===
===Outcomes===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
$this->getDataGenerator()->create_grade_outcome();
$this->getDataGenerator()->create_grade_outcome();
$this->getDataGenerator()->create_grade_item(array('fullname' => $fullname));
$this->getDataGenerator()->create_grade_item(array('fullname' => $fullname));
</syntaxhighlight>
</syntaxhighlight>
==Other types of plugin==
==Other types of plugin==
{{Moodle 2.5}}
{{Moodle 2.5}}
Line 270: Line 210:


For some types of plugin, like mod documented above, there may be a more specific class than component_generator_base to extend, like testing_module_generator. That will give a consistent set of method names to use. Otherwise, you can create whatever methods you like on your generator, to create the different things you need to work whith.
For some types of plugin, like mod documented above, there may be a more specific class than component_generator_base to extend, like testing_module_generator. That will give a consistent set of method names to use. Otherwise, you can create whatever methods you like on your generator, to create the different things you need to work whith.
=Long tests=
=Long tests=
All standard test should execute as fast as possible. Tests that take a loner time to execute (>10s) or are otherwise expensive (such as querying external servers that might be flooded by all dev machines) should be execute only when PHPUNIT_LONGTEST is true. This constant can be set in phpunit.xml or directly in config.php.
All standard test should execute as fast as possible. Tests that take a loner time to execute (>10s) or are otherwise expensive (such as querying external servers that might be flooded by all dev machines) should be execute only when PHPUNIT_LONGTEST is true. This constant can be set in phpunit.xml or directly in config.php.
=Large test data=
=Large test data=
See advanced_testcase::createXMLDataSet() and advanced_testcase::createCsvDataSet() and related functions there for easier ways to manage large test data sets within files rather than arrays in code. See [[PHPUnit_integration#Extra_methods]]
See advanced_testcase::createXMLDataSet() and advanced_testcase::createCsvDataSet() and related functions there for easier ways to manage large test data sets within files rather than arrays in code. See [[PHPUnit_integration#Extra_methods]]
=Testing sending of messages=
=Testing sending of messages=
{{Moodle 2.4}}
{{Moodle 2.4}}
Line 291: Line 227:
//.. test messages were generated in correct order with appropriate content
//.. test messages were generated in correct order with appropriate content
</syntaxhighlight>
</syntaxhighlight>
=Testing sending of emails=
=Testing sending of emails=
{{Moodle 2.6}}
{{Moodle 2.6}}
Line 304: Line 239:
$this->assertEquals(1, count($messages));
$this->assertEquals(1, count($messages));
</syntaxhighlight>
</syntaxhighlight>
=Logstores=
=Logstores=
You can test events which were written to a logstore, but you must disable transactions, enable at least one valid logstore, and disable logstore buffering to ensure that the events are written to the database before the tests execute.
You can test events which were written to a logstore, but you must disable transactions, enable at least one valid logstore, and disable logstore buffering to ensure that the events are written to the database before the tests execute.
Line 313: Line 247:
get_log_manager(true);
get_log_manager(true);
</syntaxhighlight>
</syntaxhighlight>
=Check your coverage=
=Check your coverage=
{{Moodle 3.7}}
{{Moodle 3.7}}
Line 321: Line 254:


Since Moodle 3.7 the '''phpunit.xml''' configuration contains generated coverage include and exclude information for each component.
Since Moodle 3.7 the '''phpunit.xml''' configuration contains generated coverage include and exclude information for each component.
==Generating include and exclude configuration==
==Generating include and exclude configuration==
{{Moodle 3.11}}
{{Moodle 3.11}}
You can programatically describe which files will be checked for coverage by creating a <tt>coverage.php</tt> file alongside the tests that you are writing.
You can programatically describe which files will be checked for coverage by creating a <tt>coverage.php</tt> file alongside the tests that you are writing.
Since Moodle 4.0, a default configuration is applied for all plugins and it is not necessary to supply a coverage.php unless you wish to cover additional files.


The <tt>coverage.php</tt> file allows you to list include and exclude files and folders within the component being
The <tt>coverage.php</tt> file allows you to list include and exclude files and folders within the component being
Line 331: Line 265:
for this would be in '''mod/forum/tests/coverage.php''' and all paths specified would be relative to '''mod/forum'''.
for this would be in '''mod/forum/tests/coverage.php''' and all paths specified would be relative to '''mod/forum'''.


It is possible to specify a combination of included files, included folders, excluded files, and
It is possible to specify a combination of included files, included folders, excluded files, and excluded folders. This would allow you, for example, to include the entire '''classes''' directory, but exclude.a specific file or folder within it.
excluded folders. This would allow you, for example, to include the entire '''classes''' directory, but exclude
a specific file or folder within it.


The following is an example <tt>coverage.php</tt> file from '''mod_forum''':
The following is an example <tt>coverage.php</tt> file from '''mod_forum''':


Note: For Moodle versions 3.7 to 3.10, the [https://docs.moodle.org/dev/index.php?title=Writing_PHPUnit_tests&oldid=58177#Check_your_coverage syntax used] was slightly different.
Note: For Moodle versions 3.7 to 3.10, the [https://docs.moodle.org/dev/index.php?title=Writing_PHPUnit_tests&oldid=58177#Check_your_coverage syntax used] was slightly different.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
return new class extends phpunit_coverage_info {
return new class extends phpunit_coverage_info {
Line 357: Line 288:
};
};
</syntaxhighlight>
</syntaxhighlight>
Also, note that you can better define which class or function each test is effectively covering by using the <tt>@covers</tt> annotation as [https://phpunit.readthedocs.io/en/9.5/code-coverage-analysis.html#specifying-covered-code-parts described in the documention].
Since Moodle 4.0, the following default configuration is applied:{{Moodle 4.0}}
<syntaxhighlight lang="php">
return new class extends phpunit_coverage_info {
    /** @var array The list of folders relative to the plugin root to include in coverage generation. */
    protected $includelistfolders = [
        'classes',
        'tests/generator',
    ];
    /** @var array The list of files relative to the plugin root to include in coverage generation. */
    protected $includelistfiles = [
        'externallib.php',
        'lib.php',
        'locallib.php',
        'renderer.php',
        'rsslib.php',
    ];


Also, note that you can better define which class or function each test is effectively covering by using the <tt>@covers</tt> annotation as [https://phpunit.readthedocs.io/en/9.5/code-coverage-analysis.html#specifying-covered-code-parts described in the documention].
    /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfolders = [];


    /** @var array The list of files relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfiles = [];
};
</syntaxhighlight>
If a coverage.php file already exists, then the defaults will be added to the values already defined.
=Best practice=
=Best practice=
There are several best practices, suggestions, and things to avoid which you should consider when writing unit tests. Some of these are described below.
There are several best practices, suggestions, and things to avoid which you should consider when writing unit tests. Some of these are described below.
==Check your coverage==
==Check your coverage==
PHPUnit has the ability to generate code coverage information for your unit tests and this is well supported since
PHPUnit has the ability to generate code coverage information for your unit tests and this is well supported since
Moodle 3.7. We recommend that you consider checking the coverage of your plugins when you write your code.
Moodle 3.7. We recommend that you consider checking the coverage of your plugins when you write your code.
==Keep use of resetAfterTest to a minimum==
==Keep use of resetAfterTest to a minimum==
Although many of the examples described above use the <syntaxhighlight lang="php">resetAfterTest</syntaxhighlight> nomenclature to reset the database and filesystem after your test completes, you should ideally not use this unless you have to.
Although many of the examples described above use the <syntaxhighlight lang="php">resetAfterTest</syntaxhighlight> nomenclature to reset the database and filesystem after your test completes, you should ideally not use this unless you have to.
Generally speaking you should aim to write code which is mockable, and does not require real fixtures.
Generally speaking you should aim to write code which is mockable, and does not require real fixtures.
Use of resetAfterTest will also slow your tests down.
Use of resetAfterTest will also slow your tests down.
==Be careful with shared setUp and instance variables==
==Be careful with shared setUp and instance variables==
You should be careful of how you create and use instance variables in PHPUnit tests for two main reasons:
You should be careful of how you create and use instance variables in PHPUnit tests for two main reasons:


Line 387: Line 338:


==Make use of the dataProvider functionality==
==Make use of the dataProvider functionality==
The dataProvider functionality of PHPUnit is an extremely powerful and useful feature which allows you to verify a function quickly and easily with a range of different conditions.
The dataProvider functionality of PHPUnit is an extremely powerful and useful feature which allows you to verify a function quickly and easily with a range of different conditions.
However, the following rules should be followed when using dataProviders:
However, the following rules should be followed when using dataProviders:
* Keep addition of resettable data requring resetAfterTest to a minimum - this will lead to many slow tests
* Keep addition of resettable data requring resetAfterTest to a minimum - this will lead to many slow tests
* Data providers '''must not instantiate/create data'''. Just define it. And then, the test body can proceed with the instantiation/creation. The dataProvider is called after the testSuite is instantiated, but before any tests are run. Each test will run a full setUp and tearDown, which will destroy any data which was created.
* Data providers '''must not instantiate/create data'''. Just define it. And then, the test body can proceed with the instantiation/creation. The dataProvider is called after the testSuite is instantiated, but before any tests are run. Each test will run a full setUp and tearDown, which will destroy any data which was created.
<syntaxhighlight lang="php">
<syntaxhighlight lang="php">
/**
/**
Line 423: Line 372:
}
}
</syntaxhighlight>
</syntaxhighlight>
=Extra test settings=
=Extra test settings=
Usually the test should not interact with any external systems and it should work the same on all systems. But sometimes you need to specify some option for connection to external systems or system configuration. It is intentionally not possible to use $CFG settings from config.php.
Usually the test should not interact with any external systems and it should work the same on all systems. But sometimes you need to specify some option for connection to external systems or system configuration. It is intentionally not possible to use $CFG settings from config.php.


Line 431: Line 378:
* define test setting constants in your phpunit.xml file
* define test setting constants in your phpunit.xml file
* define test setting constants in your config.php
* define test setting constants in your config.php
These constants may be then used in your test or plugin code.
These constants may be then used in your test or plugin code.
=Upgrading unit tests to work with Moodle 3.11 and up (PHPUnit 9.5)=
=Upgrading unit tests to work with Moodle 3.11 and up (PHPUnit 9.5)=
{{Moodle 3.11}}
{{Moodle 3.11}}
Line 442: Line 387:


To find more information about the changes coming with PHPUnit 9.5, it's recommended to read the following resources:
To find more information about the changes coming with PHPUnit 9.5, it's recommended to read the following resources:
* [https://thephp.cc/news/2020/02/migrating-to-phpunit-9 A good article] explaining all the main changes in the release.
* [https://thephp.cc/news/2020/02/migrating-to-phpunit-9 A good article] explaining all the main changes in the release.
* [https://phpunit.de/announcements/phpunit-9.html PHPUnit 9 release Announcement].
* [https://phpunit.de/announcements/phpunit-9.html PHPUnit 9 release Announcement].
* These multiple detailed changelogs (because all them have included modifications requiring changes): [https://github.com/sebastianbergmann/phpunit/blob/9.0.0/ChangeLog-9.0.md 9.0], [https://github.com/sebastianbergmann/phpunit/blob/9.1.0/ChangeLog-9.1.md 9.1], [https://github.com/sebastianbergmann/phpunit/blob/9.3.0/ChangeLog-9.3.md 9.3], [https://github.com/sebastianbergmann/phpunit/blob/9.5.0/ChangeLog-9.5.md 9.5]
* These multiple detailed changelogs (because all them have included modifications requiring changes): [https://github.com/sebastianbergmann/phpunit/blob/9.0.0/ChangeLog-9.0.md 9.0], [https://github.com/sebastianbergmann/phpunit/blob/9.1.0/ChangeLog-9.1.md 9.1], [https://github.com/sebastianbergmann/phpunit/blob/9.3.0/ChangeLog-9.3.md 9.3], [https://github.com/sebastianbergmann/phpunit/blob/9.5.0/ChangeLog-9.5.md 9.5]
* [https://phpunit.readthedocs.io/en/9.5/ Official PHPUnit manual].
* [https://phpunit.readthedocs.io/en/9.5/ Official PHPUnit manual].
A good summary of all the '''changes and replacements to perform''' is available in the [https://github.com/moodle/moodle/blob/e3a46964dc6d8ca1558c6e1e8dfdf3c1745eeaed/lib/upgrade.txt#L5-L65 lib/upgrade.txt] file. With main points being:
A good summary of all the '''changes and replacements to perform''' is available in the [https://github.com/moodle/moodle/blob/e3a46964dc6d8ca1558c6e1e8dfdf3c1745eeaed/lib/upgrade.txt#L5-L65 lib/upgrade.txt] file. With main points being:
* All the changes that were deprecated with PHPUnit 8.5 (see the section below) are now removed and will lead to errors.
* All the changes that were deprecated with PHPUnit 8.5 (see the section below) are now removed and will lead to errors.
Line 459: Line 402:
* Deprecations, deprecations, deprecations. Lots of them in often used assertions: file assertions, regexp assertions, exception expectations... again, note that all them will become errors with the next PHPUnit update, so '''the recommendation is to update them ASAP'''.
* Deprecations, deprecations, deprecations. Lots of them in often used assertions: file assertions, regexp assertions, exception expectations... again, note that all them will become errors with the next PHPUnit update, so '''the recommendation is to update them ASAP'''.
Finally, it's also a good idea to [https://github.com/moodle/moodle/compare/fc335f5...713722c browse the changes associated with the issue], hopefully with useful explanations in the commit messages.
Finally, it's also a good idea to [https://github.com/moodle/moodle/compare/fc335f5...713722c browse the changes associated with the issue], hopefully with useful explanations in the commit messages.
=Upgrading unit tests to work with Moodle 3.10 and up (PHPUnit 8.5)=
=Upgrading unit tests to work with Moodle 3.10 and up (PHPUnit 8.5)=
{{Moodle 3.10}}
{{Moodle 3.10}}
Line 468: Line 410:


To find more information about the changes coming with PHPUnit 8.5, it's recommended to read the following resources:
To find more information about the changes coming with PHPUnit 8.5, it's recommended to read the following resources:
* [https://thephp.cc/news/2019/02/help-my-tests-stopped-working A good article] explaining all the main changes in the release.
* [https://thephp.cc/news/2019/02/help-my-tests-stopped-working A good article] explaining all the main changes in the release.
* [https://phpunit.de/announcements/phpunit-8.html PHPUnit 8 release Announcement].
* [https://phpunit.de/announcements/phpunit-8.html PHPUnit 8 release Announcement].
Line 478: Line 419:
** Deprecations, deprecations, deprecations. Lots of them in often used assertions (<tt>assertContains()</tt>, <tt>assertEquals()</tt>...) and also <tt>@expectedExceptionXXX</tt> annotations. Again, note that all them will become errors with PHPUnit 9.
** Deprecations, deprecations, deprecations. Lots of them in often used assertions (<tt>assertContains()</tt>, <tt>assertEquals()</tt>...) and also <tt>@expectedExceptionXXX</tt> annotations. Again, note that all them will become errors with PHPUnit 9.
* Finally, it's also a good idea to [https://github.com/moodle/moodle/compare/5903054...b13ec3c browse the changes associated with the issue], hopefully with useful enough explanations in the commit messages.
* Finally, it's also a good idea to [https://github.com/moodle/moodle/compare/5903054...b13ec3c browse the changes associated with the issue], hopefully with useful enough explanations in the commit messages.
=Upgrading unit tests to work with Moodle 3.7 and up (PHPUnit 7.5)=
=Upgrading unit tests to work with Moodle 3.7 and up (PHPUnit 7.5)=
{{Moodle 3.7}}
{{Moodle 3.7}}
Line 489: Line 429:
* [https://phpunit.readthedocs.io/en/7.5/ Official PHPUnit manual].
* [https://phpunit.readthedocs.io/en/7.5/ Official PHPUnit manual].
* Changes performed into core, mainly: new, stricter, signatures ([https://github.com/moodle/moodle/commit/26218b7 26218b7]) and assertEquals() changes, specially important when comparing strings, now performed using strict (===) equals ([https://github.com/moodle/moodle/commit/85f47ba 85f47ba]).
* Changes performed into core, mainly: new, stricter, signatures ([https://github.com/moodle/moodle/commit/26218b7 26218b7]) and assertEquals() changes, specially important when comparing strings, now performed using strict (===) equals ([https://github.com/moodle/moodle/commit/85f47ba 85f47ba]).
=Upgrading unit tests to work with Moodle 3.4 and up (PHPUnit 6)=
=Upgrading unit tests to work with Moodle 3.4 and up (PHPUnit 6)=
{{Moodle 3.4}}
{{Moodle 3.4}}
Line 502: Line 441:
* [https://github.com/sebastianbergmann/phpunit/blob/9d0c024d2099531442d862b66b0ad7cf35ed8e78/ChangeLog-6.0.md Detailed changelog of the release], paying special attention to the changed and deleted sections.
* [https://github.com/sebastianbergmann/phpunit/blob/9d0c024d2099531442d862b66b0ad7cf35ed8e78/ChangeLog-6.0.md Detailed changelog of the release], paying special attention to the changed and deleted sections.
* Changes performed into core, mainly: namespace class renaming ([https://github.com/moodle/moodle/commit/801a372dadb6e11c8781547603e3f0a59ce5638f 801a372]) and deprecated stuff ([https://github.com/moodle/moodle/commit/796e48a58bf18533bdca423fff7949ab119101c4 796e48a])
* Changes performed into core, mainly: namespace class renaming ([https://github.com/moodle/moodle/commit/801a372dadb6e11c8781547603e3f0a59ce5638f 801a372]) and deprecated stuff ([https://github.com/moodle/moodle/commit/796e48a58bf18533bdca423fff7949ab119101c4 796e48a])
=See also=
=See also=
* [[PHPUnit integration]]
* [[PHPUnit integration]]
* [[PHPUnit]]
* [[PHPUnit]]
[[Category:Unit testing]]
[[Category:Unit testing]]

Revision as of 14:35, 20 December 2021

Moodle 2.3


Moodle PHPUnit integration is designed to allow easy adding of new tests. At the start of each test the state is automatically reset to fresh new installation (unless explicitly told not to reset).

Namespaces

All the stuff under **/tests directories is subject to some simple rules when using namespaces. They apply to test cases, fixtures, generators and, in general, any class within those directories. Take a look to them! (grand summary = 100% the same rules that are applied to **/classes directories).

Testcase classes

There are three basic test class that are supposed to used in all Moodle unit tests - basic_testcase, advanced_testcase and provider_testcase. Please note it is strongly recommended to put only one testcase into each class file.

basic_testcase
Very simple tests that do not modify database, dataroot or any PHP globals. It can be used for example when trying examples from the official PHPUnit tutorial.
advanced_testcase
Enhanced testcase class enhanced for easy testing of Moodle code.
provider_testcase
Enhanced testcase class, enhanced for easy testing of Privacy Providers.

There is a fourth testcase class that is specially designed for testing of our Moodle database layer, it should not be used for other purposes.

Assertions

The complete list of assertions can be found in the links below.

Moodle version PHPUnit version Links
Moodle 3.11 PHPUnit 9.5 Documentation
Moodle 3.10 PHPUnit 8.5 Documentation
Moodle 3.7 - 3.9 PHPUnit 7.5 Documentation
Moodle 3.4 - 3.6 PHPUnit 6.5 Documentation

Sample plugin testcase

PHPUnit tests are located in tests/*_test.php files in your plugin, for example mod/myplugin/tests/sample_test.php, the file should contain only one class that extends advanced_testcase:

 namespace mod_myplugin;

 class sample_test extends \advanced_testcase {
     public function test_adding() {
         $this->assertEquals(2, 1+2);
     }
 }

See PHPUnit integration#Class and file naming rules for more information.

Inclusion of Moodle library files

If you want to include some Moodle library files you should always declare global $CFG. The reason is that testcase files may be included from non-moodle code which does not make the global $CFG available automatically.

Automatic state reset

By default after each test Moodle database and dataroot is automatically reset to the original state which was present right after installation. make sure to use $this->resetAfterTest() to indicate that the database or changes of standard global variables are expected.

If you received the error "Warning: unexpected database modification, resetting DB state" it is because the test is not using $this->resetAfterTest().

 namespace mod_myplugin;

 class test_something extends \advanced_testcase {
     public function test_deleting() {
         global $DB;
         $this->resetAfterTest(true);
         $DB->delete_records('user');
         $this->assertEmpty($DB->get_records('user'));
     }
     public function test_user_table_was_reset() {
         global $DB;
         $this->assertEquals(2, $DB->count_records('user', array()));
     }
 }

Generators

Tests that need to modify default installation may use generators to create new courses, users, etc. All examples on this page should be used from test methods of a test class derived from advanced_testcase.

Note if you are using PHPUnit @dataProvider functions to provide parameters to unit tests, you can not use the data generator or change the user etc in the data provider function. Data providers must not instantiate/create data. Just define it. And then, the test body can proceed with the instantiation/creation.

Creating users

At the start of each test there are only two users present - guest and administrator. If you need to add more test accounts use:

 $user = $this->getDataGenerator()->create_user();

You may also specify properties of the user account, for example:

 $user1 = $this->getDataGenerator()->create_user(array('email'=>'user1@example.com', 'username'=>'user1'));

By default no user is logged-in, use setUser() method to change current $USER value:

 $this->setUser($user1);

Guest and admin accounts have a shortcut methods:

 $this->setGuestUser();
 $this->setAdminUser();

Null can be used to set current user back to not-logged-in:

 $this->setUser(null);

Creating course categories

 $category1 = $this->getDataGenerator()->create_category();
 $category2 = $this->getDataGenerator()->create_category(array('name'=>'Some subcategory', 'parent'=>$category1->id));

Creating courses

 $course1 = $this->getDataGenerator()->create_course();
 
 $category = $this->getDataGenerator()->create_category();
 $course2 = $this->getDataGenerator()->create_course(array('name'=>'Some course', 'category'=>$category->id));

Creating activities

Some activity plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php.

Example of creation of new course with one page resource:

 $course = $this->getDataGenerator()->create_course();
 $generator = $this->getDataGenerator()->get_plugin_generator('mod_page');
 $generator->create_instance(array('course'=>$course->id));

The following is functionally the same, but a bit shorter:

 $course = $this->getDataGenerator()->create_course();
 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));

Creating cohorts

Moodle 2.4

Since 2.4 there the data generator supports creation of new cohorts.

 $cohort = $this->getDataGenerator()->create_cohort();

Simplified user enrolments

Moodle 2.4

Instead of standard enrolment API it is possible to use simplified method in data generator. It is intended to be used with self and manual enrolment plugins.

$this->getDataGenerator()->enrol_user($userid, $courseid);
$this->getDataGenerator()->enrol_user($userid, $courseid, $teacherroleid);
$this->getDataGenerator()->enrol_user($userid, $courseid, $teacherroleid, 'manual');

Creating scales

$this->getDataGenerator()->create_scale();
$this->getDataGenerator()->create_scale(array('name' => $name, 'scale' => $scale, 'courseid' => $courseid, 'userid' => $userid, 'description' => description, 'descriptionformat' => $descriptionformat));

Creating roles

$this->getDataGenerator()->create_role();
$this->getDataGenerator()->create_role(array('shortname' => $shortname, 'name' => $name, 'description' => description, 'archetype' => $archetype));

Creating tags

$this->getDataGenerator()->create_tag();
$this->getDataGenerator()->create_tag(array(
    'userid' => $userid, 
    'rawname' => $rawname,
    'name' => $name, 
    'description' => $description, 
    'descriptionformat' => $descriptionformat,
    'flag' => $flag
));

Groups

Creating groups

$this->getDataGenerator()->create_group(array('courseid' => $courseid));
$this->getDataGenerator()->create_group(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));

Adding users to groups

$this->getDataGenerator()->create_group_member(array('userid' => $userid, 'groupid' => $groupid));
$this->getDataGenerator()->create_group_member(array('userid' => $userid, 'groupid' => $groupid, 'component' => $component, 'itemid' => $itemid));

Creating groupings

$this->getDataGenerator()->create_grouping(array('courseid' => $courseid));
$this->getDataGenerator()->create_grouping(array('courseid' => $courseid, 'name' => $name, 'description' => $description, 'descriptionformat' => $descriptionformat));

Adding groups to groupings

$this->getDataGenerator()->create_grouping_group(array('groupingid' => $groupingid, 'groupid' => $groupid));

Repositories

Creating repository instances

Moodle 2.5

Some respository plugins include instance generators. The generator class are defined in plugindirectory/tests/generator/lib.php..

$this->getDataGenerator()->create_repository($type, $record, $options);

Creating repository types

Moodle 2.5

Some respository plugins include type generators. The generator class are defined in plugindirectory/tests/generator/lib.php..

$this->getDataGenerator()->create_repository_type($type, $record, $options);

Creating grades

Grade categories

$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid));
$this->getDataGenerator()->create_grade_category(array('courseid' => $courseid, 'fullname' => $fullname));

Grade items

$this->getDataGenerator()->create_grade_item();
$this->getDataGenerator()->create_grade_item(array('itemtype' => $itemtype, 'itemname' => $itemname, 'outcomeid' => $outcomeid, 'scaleid' => $scaleid, 'gradetype' => $gradetype));

Outcomes

$this->getDataGenerator()->create_grade_outcome();
$this->getDataGenerator()->create_grade_item(array('fullname' => $fullname));

Other types of plugin

Moodle 2.5

Any other type of plugin can have a generator. The generator class should extend component_generator_base, and then you can get an instance using $mygenerator = $this->getDataGenerator()->get_plugin_generator($frankenstylecomponentname);

For some types of plugin, like mod documented above, there may be a more specific class than component_generator_base to extend, like testing_module_generator. That will give a consistent set of method names to use. Otherwise, you can create whatever methods you like on your generator, to create the different things you need to work whith.

Long tests

All standard test should execute as fast as possible. Tests that take a loner time to execute (>10s) or are otherwise expensive (such as querying external servers that might be flooded by all dev machines) should be execute only when PHPUNIT_LONGTEST is true. This constant can be set in phpunit.xml or directly in config.php.

Large test data

See advanced_testcase::createXMLDataSet() and advanced_testcase::createCsvDataSet() and related functions there for easier ways to manage large test data sets within files rather than arrays in code. See PHPUnit_integration#Extra_methods

Testing sending of messages

Moodle 2.4

You can temporarily redirect all messages sent via message_send() to a message sink object. This allows developers to verify that the tested code is sending expected messages.

To test code using messaging first disable the use of transactions and then redirect the messaging into a new message sink, you can inspect the results later.

$this->preventResetByRollback();
$sink = $this->redirectMessages();
//... code that is sending messages
$messages = $sink->get_messages();
$this->assertEquals(3, count($messages));
//.. test messages were generated in correct order with appropriate content

Testing sending of emails

Moodle 2.6

You can temporarily redirect emails sent via email_to_user() to a email message sink object. This allows developers to verify that the tested code is sending expected emails.

To test code using messaging first unset 'noemailever' setting and then redirect the emails into a new message sink where you can inspect the results later.

unset_config('noemailever');
$sink = $this->redirectEmails();
//... code that is sending email
$messages = $sink->get_messages();
$this->assertEquals(1, count($messages));

Logstores

You can test events which were written to a logstore, but you must disable transactions, enable at least one valid logstore, and disable logstore buffering to ensure that the events are written to the database before the tests execute.

$this->preventResetByRollback();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
get_log_manager(true);

Check your coverage

Moodle 3.7

PHPUnit has the ability to generate code coverage information for your unit tests.

Prior to Moodle 3.7, this coverage would load all files and generate coverage for everything regardless of whether that file could be covered at all, or whether it was intentionally covered.

Since Moodle 3.7 the phpunit.xml configuration contains generated coverage include and exclude information for each component.

Generating include and exclude configuration

Moodle 3.11

You can programatically describe which files will be checked for coverage by creating a coverage.php file alongside the tests that you are writing.

Since Moodle 4.0, a default configuration is applied for all plugins and it is not necessary to supply a coverage.php unless you wish to cover additional files.

The coverage.php file allows you to list include and exclude files and folders within the component being tested. All paths specified are relative to the component being tested. For example, when working with mod_forum your code will be in mod/forum, and its unit tests will be in mod/forum/tests/example_test.php. The coverage file for this would be in mod/forum/tests/coverage.php and all paths specified would be relative to mod/forum.

It is possible to specify a combination of included files, included folders, excluded files, and excluded folders. This would allow you, for example, to include the entire classes directory, but exclude.a specific file or folder within it.

The following is an example coverage.php file from mod_forum:

Note: For Moodle versions 3.7 to 3.10, the syntax used was slightly different.

return new class extends phpunit_coverage_info {
    /** @var array The list of folders relative to the plugin root to include in coverage generation. */
    protected $includelistfolders = [
        'classes',
        'externallib.php',
    ];

    /** @var array The list of files relative to the plugin root to include in coverage generation. */
    protected $includelistfiles = [];

    /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfolders = [];

    /** @var array The list of files relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfiles = [];
};

Also, note that you can better define which class or function each test is effectively covering by using the @covers annotation as described in the documention.

Since Moodle 4.0, the following default configuration is applied:Moodle 4.0

return new class extends phpunit_coverage_info {
    /** @var array The list of folders relative to the plugin root to include in coverage generation. */
    protected $includelistfolders = [
        'classes',
        'tests/generator',
    ];

    /** @var array The list of files relative to the plugin root to include in coverage generation. */
    protected $includelistfiles = [
        'externallib.php',
        'lib.php',
        'locallib.php',
        'renderer.php',
        'rsslib.php',
    ];

    /** @var array The list of folders relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfolders = [];

    /** @var array The list of files relative to the plugin root to exclude from coverage generation. */
    protected $excludelistfiles = [];
};

If a coverage.php file already exists, then the defaults will be added to the values already defined.

Best practice

There are several best practices, suggestions, and things to avoid which you should consider when writing unit tests. Some of these are described below.

Check your coverage

PHPUnit has the ability to generate code coverage information for your unit tests and this is well supported since Moodle 3.7. We recommend that you consider checking the coverage of your plugins when you write your code.

Keep use of resetAfterTest to a minimum

Although many of the examples described above use the

resetAfterTest

nomenclature to reset the database and filesystem after your test completes, you should ideally not use this unless you have to.

Generally speaking you should aim to write code which is mockable, and does not require real fixtures. Use of resetAfterTest will also slow your tests down.

Be careful with shared setUp and instance variables

You should be careful of how you create and use instance variables in PHPUnit tests for two main reasons:

Firstly, if you create any fixtures in the setUp, or call the resetAfterTest function, these fixtures and conditions will apply for _all_ tests in the testsuite. You will not be able to add another test to the suite which does not require these conditions without those conditions being fulfilled anyway. This can lead to slow tests.

Secondly, because of the way in which PHPUnit operates. it creates an instance of each testcase during its bootstrap phase. These are stored in memory until the _entire suite_ completes. This means that any fixture which is setup and not actively discarded will not be garbage collected and lead to memory bloat. In severe cases this can lead to memory exhaustion.


Make use of the dataProvider functionality

The dataProvider functionality of PHPUnit is an extremely powerful and useful feature which allows you to verify a function quickly and easily with a range of different conditions. However, the following rules should be followed when using dataProviders:

  • Keep addition of resettable data requring resetAfterTest to a minimum - this will lead to many slow tests
  • Data providers must not instantiate/create data. Just define it. And then, the test body can proceed with the instantiation/creation. The dataProvider is called after the testSuite is instantiated, but before any tests are run. Each test will run a full setUp and tearDown, which will destroy any data which was created.
/**
 * Test function accepts parameters passed from the specified data provider.
 *
 * @dataProvider foobar_provider
 * @param int $foor
 * @param int $bar
 */
public function test_foobar(int $foo, int $bar) {
    // Perform the tests here.
}

/**
 * Data provider for {@see self::test_foobar()}.
 *
 * @return array List of data sets - (string) data set name => (array) data
 */
public function foobar_provider(): array {
    return [
        'Same numbers' => [
            'foo' => 42,
            'bar' => 42,
        ],
        'Different numbers' => [
            'foo' => 21,
            'bar' => 84,
        ],
    ];
}

Extra test settings

Usually the test should not interact with any external systems and it should work the same on all systems. But sometimes you need to specify some option for connection to external systems or system configuration. It is intentionally not possible to use $CFG settings from config.php.

There are several ways how to inject your custom settings:

  • define test setting constants in your phpunit.xml file
  • define test setting constants in your config.php

These constants may be then used in your test or plugin code.

Upgrading unit tests to work with Moodle 3.11 and up (PHPUnit 9.5)

Moodle 3.11


With Moodle 3.11, PHPUnit was upgraded to 9.5 (from 8.5 being used in previous versions). This was done to better align the testing environment with PHP versions supported by Moodle 3.11 (7.3, 7.4 and 8.0) (see MDL-71036 and linked issues for more details).

While a lot of existing tests will work without modification with PHPUnit 9.5, you will get a good number of deprecation warnings ("W" in the tests output) that should be replaced by their new counterparts as soon as possible, because all those warnings will become errors with next PHPUnit upgrade.

To find more information about the changes coming with PHPUnit 9.5, it's recommended to read the following resources:

A good summary of all the changes and replacements to perform is available in the lib/upgrade.txt file. With main points being:

  • All the changes that were deprecated with PHPUnit 8.5 (see the section below) are now removed and will lead to errors.
  • assertContains() now performs stricter comparison (like assertSame() does). New assertContainsEquals() has been created to provide the old behavior.
  • Changes to the phpunit.xml schema, mostly internal. These only will impact if you are using custom phpunit.xml files:
    • The previous <filter> section is now (better) called <coverage>. And, within it:
      • <whitelist> has been replaced by <include>.
      • <exclude> is not a child of <whitelist> anymore, but of <coverage>.
    • But with implications when defining the coverage information because $whitelistxxx properties used by the coverage.php files have been deprecated, instead use includelistfolders and includelistfiles (to better map the elements in the xml).
  • Warning: It's not possible to run individual test files any more. Use any of the alternative execution methods (filter, suite, config) to specify which tests you want to run. This will be hopefully fixed in MDL-71049 once it has been agreed which the best way to proceed is.
  • Deprecations, deprecations, deprecations. Lots of them in often used assertions: file assertions, regexp assertions, exception expectations... again, note that all them will become errors with the next PHPUnit update, so the recommendation is to update them ASAP.

Finally, it's also a good idea to browse the changes associated with the issue, hopefully with useful explanations in the commit messages.

Upgrading unit tests to work with Moodle 3.10 and up (PHPUnit 8.5)

Moodle 3.10


With Moodle 3.10, PHPUnit was upgraded to 8.5 (from 7.5 being used in older version). This was done to better align the testing environment with PHP versions supported by Moodle 3.10 (7.2, 7.3 and 7.4) and also to provide an easier jump to PHPUnit 9.x that will be needed for Moodle 3.11 (php versions 7.3, 7.4 and 8.0), removing a lot of deprecated stuff in advance. (see MDL-67673 and MDL-64600 for more details).

While 99% of existing tests will work without modification with PHPUnit 8.5, you will get a good number of deprecation warnings ("W" in the tests output) that should be replaced by their new counterparts as soon as possible, because all those warnings will become errors with next PHPUnit upgrade.

To find more information about the changes coming with PHPUnit 8.5, it's recommended to read the following resources:

  • A good article explaining all the main changes in the release.
  • PHPUnit 8 release Announcement.
  • Detailed changelog of the release.
  • Official PHPUnit manual.
  • A good summary of all the changes and replacements to perform is available in the lib/upgrade.txt file. With main points being:
    • Support for PHP 7.0 dropped (because all template methods (setUp(), tearDown()..) now require to return void. This will mostly impact 3rd-part plugins that were still running the same tests against old branches of Moodle with PHP 7.0 support.
    • PHPUnit/DBUnit has been removed are replaced by a lightweight alternative.
    • Deprecations, deprecations, deprecations. Lots of them in often used assertions (assertContains(), assertEquals()...) and also @expectedExceptionXXX annotations. Again, note that all them will become errors with PHPUnit 9.
  • Finally, it's also a good idea to browse the changes associated with the issue, hopefully with useful enough explanations in the commit messages.

Upgrading unit tests to work with Moodle 3.7 and up (PHPUnit 7.5)

Moodle 3.7


With Moodle 3.7, PHPUnit was upgraded to 7.5 (from 6.x being used in older version). This was done to better align the testing environment with PHP versions supported by Moodle 3.7 (7.1, 7.2 and 7.3). (see MDL-65204 and linked issues for more details). While internally a lot of things changed with PHPUnit 7 (PHP 7.1-isms, typed signatures, void returns, assertEquals() changes), thanks to our wrapping layer (basic and advanced testcases...) impact expected into old existing unit tests is expected to be reduced and upgrades, easy to achieve.

To find more information about the changes coming with PHPUnit 7, it's recommended to read the following resources:

Upgrading unit tests to work with Moodle 3.4 and up (PHPUnit 6)

Moodle 3.4


With Moodle 3.4, PHPUnit was upgraded to 6.4 (from 5.5 being used in older version). This was done to better align the testing environment with PHP versions supported by Moodle 3.4 (7.0, 7.1 and 7.2). (see MDL-60611 and linked issues for more details). While internally a lot of things changed with PHPUnit 6 (namespaced classes being the more noticeable), thanks to our wrapping layer (basic and advanced testcases...) impact expected into old existing unit tests is expected to be reduced and upgrades, easy to achieve.

Still, in some cases, it will impossible to maintain compatibility of tests between old (pre 3.4) tests and new ones, especially when direct use of any phpunit class is performed. Luckily, both travis and CI tests will detect this situation and it shouldn't be hard to keep all supported branches in core passing ok. Plugins may be trickier, if the same branch is attempting to work against multiple core branches and they are using some phpunit class directly.

To find more information about the changes coming with PHPUnit 6, it's recommended to read the following resources:

See also