unit tests: MDL-18607 new way to do unit tests involving the database.
This is not as ambitious as the abortive FakeDBUnitTests scheme, but this one works for simple cases. There is a new test case class UnitTestCaseUsingDatabase to inherit from. I hope it is sufficiently well documented in its PHPdocs. * It users $CFG->unittestprefix. * You can access that database using $this->testdb. * That database is empty by default, you have to call create_test_table to create the ones you want, and drop_test_table to clean them up in the end. The table definitions are read from the XMLDB file. * When you are ready to call real Moodle code that users $DB, call switch_to_test_db and then revert_to_real_db when you are done. * If you forget to call drop_test_table or switch_to_test_db, the class will attempt to clean up after you, but will also print rude developer debug messages telling you not to be stupid. * There is also a load_test_data method for populating a table from an array. The is an example of its use in lib/simpletest/testunittestusingdb.php.
This commit is contained in:
@@ -16,6 +16,10 @@ require_once($CFG->libdir.'/simpletestlib.php');
|
||||
require_once('ex_simple_test.php');
|
||||
require_once('ex_reporter.php');
|
||||
|
||||
// Always run the unit tests in developer debug mode.
|
||||
$CFG->debug = DEBUG_DEVELOPER;
|
||||
error_reporting($CFG->debug);
|
||||
|
||||
// page parameters
|
||||
$path = optional_param('path', null, PARAM_PATH);
|
||||
$showpasses = optional_param('showpasses', false, PARAM_BOOL);
|
||||
@@ -32,8 +36,6 @@ $UNITTEST = new object();
|
||||
// Print the header.
|
||||
$strtitle = get_string('unittests', $langfile);
|
||||
|
||||
unset($CFG->unittestprefix); // for now - until test_tables.php gets implemented
|
||||
|
||||
if (!is_null($path)) {
|
||||
// Turn off xmlstrictheaders during the unit test run.
|
||||
$origxmlstrictheaders = !empty($CFG->xmlstrictheaders);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
if (!defined('MOODLE_INTERNAL')) {
|
||||
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
|
||||
}
|
||||
|
||||
class UnitTestCaseUsingDatabase_test extends UnitTestCaseUsingDatabase {
|
||||
|
||||
function test_stuff() {
|
||||
global $CFG, $DB;
|
||||
$dbman = $this->testdb->get_manager();
|
||||
|
||||
$this->assertFalse($dbman->table_exists('quiz_attempts'));
|
||||
$this->assertFalse($dbman->table_exists('quiz'));
|
||||
$this->create_test_table('quiz_attempts', 'mod/quiz');
|
||||
$this->assertTrue($dbman->table_exists('quiz_attempts'));
|
||||
$this->assertFalse($dbman->table_exists('quiz'));
|
||||
|
||||
$this->load_test_data('quiz_attempts',
|
||||
array('quiz', 'uniqueid', 'attempt', 'preview', 'layout'), array(
|
||||
array( 1 , 1 , 1 , 0 , '1,2,3,0'),
|
||||
array( 1 , 2 , 2 , 1 , '2,3,1,0')));
|
||||
|
||||
$this->switch_to_test_db();
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
$this->assertTrue(quiz_has_attempts(1));
|
||||
$this->revert_to_real_db();
|
||||
|
||||
$this->drop_test_table('quiz_attempts');
|
||||
$this->assertFalse($dbman->table_exists('quiz_attempts'));
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
@@ -150,6 +150,194 @@ class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class lets you write unit tests that access a separate set of test
|
||||
* tables with a different prefix. Only those tables you explicitly ask to
|
||||
* be created will be.
|
||||
*/
|
||||
class UnitTestCaseUsingDatabase extends UnitTestCase {
|
||||
private $realdb;
|
||||
protected $testdb;
|
||||
private $tables = array();
|
||||
|
||||
/**
|
||||
* In the constructor, record the max(id) of each test table into a csv file.
|
||||
* If this file already exists, it means that a previous run of unit tests
|
||||
* did not complete, and has left data undeleted in the DB. This data is then
|
||||
* deleted and the file is retained. Otherwise it is created.
|
||||
* @throws moodle_exception if CSV file cannot be created
|
||||
*/
|
||||
public function __construct($label = false) {
|
||||
global $DB, $CFG;
|
||||
|
||||
if (empty($CFG->unittestprefix)) {
|
||||
throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
|
||||
}
|
||||
parent::UnitTestCase($label);
|
||||
|
||||
$this->realdb = $DB;
|
||||
$this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
|
||||
$this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to using the test database for all queries until further notice.
|
||||
* You must remember to switch back using revert_to_real_db() before the end of the test.
|
||||
*/
|
||||
protected function switch_to_test_db() {
|
||||
global $DB;
|
||||
if ($DB === $this->testdb) {
|
||||
debugging('switch_to_test_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
|
||||
}
|
||||
$DB = $this->testdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to using the test database for all future queries.
|
||||
*/
|
||||
protected function revert_to_real_db() {
|
||||
global $DB;
|
||||
if ($DB !== $this->testdb) {
|
||||
debugging('revert_to_real_db called when the test DB was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
|
||||
}
|
||||
$DB = $this->realdb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the user has not forgotten to clean anything up, and if they
|
||||
* have, display a rude message and clean it up for them.
|
||||
*/
|
||||
private function emergency_clean_up() {
|
||||
global $DB;
|
||||
|
||||
// Check that they did not forget to drop any test tables.
|
||||
if (!empty($this->tables)) {
|
||||
debugging('You did not clean up all your test tables in your UnitTestCaseUsingDatabase. Tables remaining: ' .
|
||||
implode(', ', array_keys($this->tables)), DEBUG_DEVELOPER);
|
||||
}
|
||||
foreach ($this->tables as $tablename => $notused) {
|
||||
$this->drop_test_table($tablename);
|
||||
}
|
||||
|
||||
// Check that they did not forget to switch page to the real DB.
|
||||
if ($DB !== $this->realdb) {
|
||||
debugging('You did not switch back to the real database in your UnitTestCaseUsingDatabase.', DEBUG_DEVELOPER);
|
||||
$this->revert_to_real_db();
|
||||
}
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
$this->emergency_clean_up();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->emergency_clean_up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test table just like a real one, getting getting the definition from
|
||||
* the specified install.xml file.
|
||||
* @param string $tablename the name of the test table.
|
||||
* @param string $installxmlfile the install.xml file in which this table is defined.
|
||||
* $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
|
||||
* so you need only specify, for example, 'mod/quiz'.
|
||||
*/
|
||||
protected function create_test_table($tablename, $installxmlfile) {
|
||||
global $CFG;
|
||||
if (isset($this->tables[$tablename])) {
|
||||
debugging('You are attempting to create test table ' . $tablename . 'again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
|
||||
return;
|
||||
}
|
||||
$dbman = $this->testdb->get_manager();
|
||||
$dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename);
|
||||
$this->tables[$tablename] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for calling create_test_table repeatedly.
|
||||
* @param array $tablenames an array of table names.
|
||||
* @param string $installxmlfile the install.xml file in which this table is defined.
|
||||
* $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
|
||||
* so you need only specify, for example, 'mod/quiz'.
|
||||
*/
|
||||
protected function create_test_tables($tablenames, $installxmlfile) {
|
||||
foreach ($tablenames as $tablename) {
|
||||
$this->create_test_table($tablename, $installxmlfile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a test table.
|
||||
* @param $tablename the name of the test table.
|
||||
*/
|
||||
protected function drop_test_table($tablename) {
|
||||
if (!isset($this->tables[$tablename])) {
|
||||
debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
|
||||
return;
|
||||
}
|
||||
$dbman = $this->testdb->get_manager();
|
||||
$table = new xmldb_table($tablename);
|
||||
$dbman->drop_table($table);
|
||||
unset($this->tables[$tablename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for calling drop_test_table repeatedly.
|
||||
* @param array $tablenames an array of table names.
|
||||
*/
|
||||
protected function drop_test_tables($tablenames) {
|
||||
foreach ($tablenames as $tablename) {
|
||||
$this->drop_test_table($tablename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a table with some rows of data. A typical call would look like:
|
||||
*
|
||||
* $config = $this->load_test_data('config_plugins',
|
||||
* array('plugin', 'name', 'value'), array(
|
||||
* array('frog', 'numlegs', 2),
|
||||
* array('frog', 'sound', 'croak'),
|
||||
* array('frog', 'action', 'jump'),
|
||||
* ));
|
||||
*
|
||||
* @param string $table the table name.
|
||||
* @param array $cols the columns to fill.
|
||||
* @param array $data the data to load.
|
||||
* @return array $objects corresponding to $data.
|
||||
*/
|
||||
protected function load_test_data($table, array $cols, array $data) {
|
||||
$results = array();
|
||||
foreach ($data as $rowid => $row) {
|
||||
$obj = new stdClass;
|
||||
foreach ($cols as $key => $colname) {
|
||||
$obj->$colname = $row[$key];
|
||||
}
|
||||
$obj->id = $this->testdb->insert_record($table, $obj);
|
||||
$results[$rowid] = $obj;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up data loaded with load_test_data. The call corresponding to the
|
||||
* example load above would be:
|
||||
*
|
||||
* $this->delete_test_data('config_plugins', $config);
|
||||
*
|
||||
* @param string $table the table name.
|
||||
* @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
|
||||
*/
|
||||
protected function delete_test_data($table, array $rows) {
|
||||
$ids = array();
|
||||
foreach ($rows as $row) {
|
||||
$ids[] = $row->id;
|
||||
}
|
||||
$this->testdb->delete_records_list($table, 'id', $ids);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDBUnitTestCase extends UnitTestCase {
|
||||
public $tables = array();
|
||||
public $pkfile;
|
||||
|
||||
@@ -595,17 +595,18 @@ class TestSuite {
|
||||
for ($i = 0, $count = count($this->_test_cases); $i < $count; $i++) {
|
||||
if (is_string($this->_test_cases[$i])) {
|
||||
$class = $this->_test_cases[$i];
|
||||
$test = &new $class();
|
||||
// moodle hack start
|
||||
// moodle hack start - need to do this before the constructor call, because of FakeDBUnitTestCase.
|
||||
global $CFG;
|
||||
if (empty($CFG->unittestprefix)) {
|
||||
if ($test instanceof FakeDBUnitTestCase) {
|
||||
// do not execute this test because test tables not present!
|
||||
unset($test);
|
||||
continue;
|
||||
}
|
||||
if (is_subclass_of($class, 'FakeDBUnitTestCase')) {
|
||||
// Do not execute this test because the test tables system no longer works.
|
||||
continue;
|
||||
}
|
||||
if (is_subclass_of($class, 'UnitTestCaseUsingDatabase') && empty($CFG->unittestprefix)) {
|
||||
// Do not execute this test because $CFG->unittestprefix is not set, but it will be required.
|
||||
continue;
|
||||
}
|
||||
// moodle hack end
|
||||
$test = &new $class();
|
||||
$test->run($reporter);
|
||||
unset($test);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user