From f68cb08bbe362baa8f8c7ec08a4402669f848c96 Mon Sep 17 00:00:00 2001 From: tjhunt Date: Mon, 23 Mar 2009 04:12:37 +0000 Subject: [PATCH] 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. --- admin/report/unittest/index.php | 6 +- lib/simpletest/testunittestusingdb.php | 33 +++++ lib/simpletestlib.php | 188 +++++++++++++++++++++++++ lib/simpletestlib/test_case.php | 17 +-- 4 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 lib/simpletest/testunittestusingdb.php diff --git a/admin/report/unittest/index.php b/admin/report/unittest/index.php index d6cd198adc..aaa76e377b 100644 --- a/admin/report/unittest/index.php +++ b/admin/report/unittest/index.php @@ -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); diff --git a/lib/simpletest/testunittestusingdb.php b/lib/simpletest/testunittestusingdb.php new file mode 100644 index 0000000000..523ace01fa --- /dev/null +++ b/lib/simpletest/testunittestusingdb.php @@ -0,0 +1,33 @@ +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')); + } + +} +?> diff --git a/lib/simpletestlib.php b/lib/simpletestlib.php index 196fa5f247..6a6680f0a9 100644 --- a/lib/simpletestlib.php +++ b/lib/simpletestlib.php @@ -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; diff --git a/lib/simpletestlib/test_case.php b/lib/simpletestlib/test_case.php index 3c97b163f4..8f4e1bd942 100644 --- a/lib/simpletestlib/test_case.php +++ b/lib/simpletestlib/test_case.php @@ -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 { -- 2.39.5