2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Utility functions to make unit testing easier.
20 * These functions, particularly the the database ones, are quick and
21 * dirty methods for getting things done in test cases. None of these
22 * methods should be used outside test code.
25 * - T.J.Hunt@open.ac.uk
28 * @subpackage unittest
29 * @copyright © 2006 The Open University
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 defined('MOODLE_INTERNAL') || die();
38 require_once($CFG->libdir . '/simpletestlib/simpletest.php');
39 require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
40 require_once($CFG->libdir . '/simpletestlib/expectation.php');
41 require_once($CFG->libdir . '/simpletestlib/reporter.php');
42 require_once($CFG->libdir . '/simpletestlib/web_tester.php');
43 require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
46 * Recursively visit all the files in the source tree. Calls the callback
47 * function with the pathname of each file found.
49 * @param $path the folder to start searching from.
50 * @param $callback the function to call with the name of each file found.
51 * @param $fileregexp a regexp used to filter the search (optional).
52 * @param $exclude If true, pathnames that match the regexp will be ingored. If false,
53 * only files that match the regexp will be included. (default false).
54 * @param array $ignorefolders will not go into any of these folders (optional).
56 function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
57 $files = scandir($path);
59 foreach ($files as $file) {
60 $filepath = $path .'/'. $file;
61 if (strpos($file, '.') === 0) {
62 /// Don't check hidden files.
64 } else if (is_dir($filepath)) {
65 if (!in_array($filepath, $ignorefolders)) {
66 recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
68 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
69 call_user_func($callback, $filepath);
75 * An expectation for comparing strings ignoring whitespace.
78 * @subpackage simpletestex
79 * @copyright © 2006 The Open University
80 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
82 class IgnoreWhitespaceExpectation extends SimpleExpectation {
85 function IgnoreWhitespaceExpectation($content, $message = '%s') {
86 $this->SimpleExpectation($message);
87 $this->expect=$this->normalise($content);
91 return $this->normalise($ip)==$this->expect;
94 function normalise($text) {
95 return preg_replace('/\s+/m',' ',trim($text));
98 function testMessage($ip) {
99 return "Input string [$ip] doesn't match the required value.";
104 * An Expectation that two arrays contain the same list of values.
106 * @package moodlecore
107 * @subpackage simpletestex
108 * @copyright © 2006 The Open University
109 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
111 class ArraysHaveSameValuesExpectation extends SimpleExpectation {
114 function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
115 $this->SimpleExpectation($message);
116 if (!is_array($expected)) {
117 trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
118 'with an expected value that is not an array.');
120 $this->expect = $this->normalise($expected);
123 function test($actual) {
124 return $this->normalise($actual) == $this->expect;
127 function normalise($array) {
132 function testMessage($actual) {
133 return 'Array [' . implode(', ', $actual) .
134 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
140 * An Expectation that compares to objects, and ensures that for every field in the
141 * expected object, there is a key of the same name in the actual object, with
142 * the same value. (The actual object may have other fields to, but we ignore them.)
144 * @package moodlecore
145 * @subpackage simpletestex
146 * @copyright © 2006 The Open University
147 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
149 class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
152 function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
153 $this->SimpleExpectation($message);
154 if (!is_object($expected)) {
155 trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
156 'with an expected value that is not an object.');
158 $this->expect = $expected;
161 function test($actual) {
162 foreach ($this->expect as $key => $value) {
163 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
165 } else if (is_null($value) && is_null($actual->$key)) {
174 function testMessage($actual) {
175 $mismatches = array();
176 foreach ($this->expect as $key => $value) {
177 if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
179 } else if (is_null($value) && is_null($actual->$key)) {
181 } else if (!isset($actual->$key)) {
182 $mismatches[] = $key . ' (expected [' . $value . '] but was missing.';
184 $mismatches[] = $key . ' (expected [' . $value . '] got [' . $actual->$key . '].';
187 return 'Actual object does not have all the same fields with the same values as the expected object (' .
188 implode(', ', $mismatches) . ').';
192 abstract class XMLStructureExpectation extends SimpleExpectation {
194 * Parse a string as XML and return a DOMDocument;
196 * @return unknown_type
198 protected function load_xml($html) {
199 $prevsetting = libxml_use_internal_errors(true);
200 $parser = new DOMDocument();
201 if (!$parser->loadXML('<html>' . $html . '</html>')) {
202 $parser = new DOMDocument();
204 libxml_clear_errors();
205 libxml_use_internal_errors($prevsetting);
209 function testMessage($html) {
210 $parsererrors = $this->load_xml($html);
211 if (is_array($parsererrors)) {
212 foreach ($parsererrors as $key => $message) {
213 $parsererrors[$key] = $message->message;
215 return 'Could not parse XML [' . $html . '] errors were [' .
216 implode('], [', $parsererrors) . ']';
218 return $this->customMessage($html);
222 * An Expectation that looks to see whether some HMTL contains a tag with a certain attribute.
224 * @copyright 2009 Tim Hunt
225 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
227 class ContainsTagWithAttribute extends XMLStructureExpectation {
229 protected $attribute;
232 function __construct($tag, $attribute, $value, $message = '%s') {
233 parent::__construct($message);
235 $this->attribute = $attribute;
236 $this->value = $value;
239 function test($html) {
240 $parser = $this->load_xml($html);
241 if (is_array($parser)) {
244 $list = $parser->getElementsByTagName($this->tag);
246 foreach ($list as $node) {
247 if ($node->attributes->getNamedItem($this->attribute)->nodeValue === (string) $this->value) {
254 function customMessage($html) {
255 return 'Content [' . $html . '] does not contain the tag [' .
256 $this->tag . '] with attribute [' . $this->attribute . '="' . $this->value . '"].';
261 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
262 * All attributes must be present and their values must match the expected values.
263 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
265 * @copyright 2009 Nicolas Connault
266 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
268 class ContainsTagWithAttributes extends XMLStructureExpectation {
270 * @var string $tag The name of the Tag to search
274 * @var array $expectedvalues An associative array of parameters, all of which must be matched
276 protected $expectedvalues = array();
278 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
280 protected $forbiddenvalues = array();
282 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
284 protected $failurereason = 'nomatch';
286 function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '%s') {
287 parent::__construct($message);
289 $this->expectedvalues = $expectedvalues;
290 $this->forbiddenvalues = $forbiddenvalues;
293 function test($html) {
294 $parser = $this->load_xml($html);
295 if (is_array($parser)) {
299 $list = $parser->getElementsByTagName($this->tag);
300 $foundamatch = false;
302 // Iterating through inputs
303 foreach ($list as $node) {
304 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
308 // For the current expected attribute under consideration, check that values match
309 $allattributesmatch = true;
311 foreach ($this->expectedvalues as $expectedattribute => $expectedvalue) {
312 if ($node->getAttribute($expectedattribute) === '' && $expectedvalue !== '') {
313 $this->failurereason = 'nomatch';
314 continue 2; // Skip this tag, it doesn't have all the expected attributes
316 if ($node->getAttribute($expectedattribute) !== (string) $expectedvalue) {
317 $allattributesmatch = false;
318 $this->failurereason = 'nomatch';
322 if ($allattributesmatch) {
325 // Now make sure this node doesn't have any of the forbidden attributes either
326 $nodeattrlist = $node->attributes;
328 foreach ($nodeattrlist as $domattrname => $domattr) {
329 if (array_key_exists($domattrname, $this->forbiddenvalues) && $node->getAttribute($domattrname) === (string) $this->forbiddenvalues[$domattrname]) {
330 $this->failurereason = "forbiddenmatch:$domattrname:" . $node->getAttribute($domattrname);
331 $foundamatch = false;
340 function customMessage($html) {
341 $output = 'Content [' . $html . '] ';
343 if (preg_match('/forbiddenmatch:(.*):(.*)/', $this->failurereason, $matches)) {
344 $output .= "contains the tag $this->tag with the forbidden attribute=>value pair: [$matches[1]=>$matches[2]]";
345 } else if ($this->failurereason == 'nomatch') {
346 $output .= 'does not contain the tag [' . $this->tag . '] with attributes [';
347 foreach ($this->expectedvalues as $var => $val) {
348 $output .= "$var=\"$val\" ";
350 $output = rtrim($output);
359 * An Expectation that looks to see whether some HMTL contains a tag with an array of attributes.
360 * All attributes must be present and their values must match the expected values.
361 * A third parameter can be used to specify attribute=>value pairs which must not be present in a positive match.
363 * @copyright 2010 The Open University
364 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
366 class ContainsSelectExpectation extends XMLStructureExpectation {
368 * @var string $tag The name of the Tag to search
372 * @var array $expectedvalues An associative array of parameters, all of which must be matched
376 * @var array $forbiddenvalues An associative array of parameters, none of which must be matched
380 * @var string $failurereason The reason why the test failed: nomatch or forbiddenmatch
384 function __construct($name, $choices, $selected = null, $enabled = null, $message = '%s') {
385 parent::__construct($message);
387 $this->choices = $choices;
388 $this->selected = $selected;
389 $this->enabled = $enabled;
392 function test($html) {
393 $parser = $this->load_xml($html);
394 if (is_array($parser)) {
398 $list = $parser->getElementsByTagName('select');
400 // Iterating through inputs
401 foreach ($list as $node) {
402 if (empty($node->attributes) || !is_a($node->attributes, 'DOMNamedNodeMap')) {
406 if ($node->getAttribute('name') != $this->name) {
410 if ($this->enabled === true && $node->getAttribute('disabled')) {
412 } else if ($this->enabled === false && $node->getAttribute('disabled') != 'disabled') {
416 $options = $node->getElementsByTagName('option');
417 reset($this->choices);
418 foreach ($options as $option) {
419 if ($option->getAttribute('value') != key($this->choices)) {
422 if ($option->firstChild->wholeText != current($this->choices)) {
425 if ($option->getAttribute('value') === $this->selected &&
426 !$option->hasAttribute('selected')) {
429 next($this->choices);
431 if (current($this->choices) !== false) {
432 // The HTML did not contain all the choices.
440 function customMessage($html) {
441 if ($this->enabled === true) {
442 $state = 'an enabled';
443 } else if ($this->enabled === false) {
444 $state = 'a disabled';
448 $output = 'Content [' . $html . '] does not contain ' . $state .
449 ' <select> with name ' . $this->name . ' and choices ' .
450 implode(', ', $this->choices);
451 if ($this->selected) {
452 $output .= ' with ' . $this->selected . ' selected).';
460 * The opposite of {@link ContainsTagWithAttributes}. The test passes only if
461 * the HTML does not contain a tag with the given attributes.
463 * @copyright 2010 The Open University
464 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
466 class DoesNotContainTagWithAttributes extends ContainsTagWithAttributes {
467 function __construct($tag, $expectedvalues, $message = '%s') {
468 parent::__construct($tag, $expectedvalues, array(), $message);
470 function test($html) {
471 return !parent::test($html);
473 function customMessage($html) {
474 $output = 'Content [' . $html . '] ';
476 $output .= 'contains the tag [' . $this->tag . '] with attributes [';
477 foreach ($this->expectedvalues as $var => $val) {
478 $output .= "$var=\"$val\" ";
480 $output = rtrim($output);
488 * An Expectation that looks to see whether some HMTL contains a tag with a certain text inside it.
490 * @copyright 2009 Tim Hunt
491 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
493 class ContainsTagWithContents extends XMLStructureExpectation {
497 function __construct($tag, $content, $message = '%s') {
498 parent::__construct($message);
500 $this->content = $content;
503 function test($html) {
504 $parser = $this->load_xml($html);
505 $list = $parser->getElementsByTagName($this->tag);
507 foreach ($list as $node) {
508 if ($node->textContent == $this->content) {
516 function testMessage($html) {
517 return 'Content [' . $html . '] does not contain the tag [' .
518 $this->tag . '] with contents [' . $this->content . '].';
523 * An Expectation that looks to see whether some HMTL contains an empty tag of a specific type.
525 * @copyright 2009 Nicolas Connault
526 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
528 class ContainsEmptyTag extends XMLStructureExpectation {
531 function __construct($tag, $message = '%s') {
532 parent::__construct($message);
536 function test($html) {
537 $parser = $this->load_xml($html);
538 $list = $parser->getElementsByTagName($this->tag);
540 foreach ($list as $node) {
541 if (!$node->hasAttributes() && !$node->hasChildNodes()) {
549 function testMessage($html) {
550 return 'Content ['.$html.'] does not contain the empty tag ['.$this->tag.'].';
556 * Simple class that implements the {@link moodle_recordset} API based on an
557 * array of test data.
559 * See the {@link question_attempt_step_db_test} class in
560 * question/engine/simpletest/testquestionattemptstep.php for an example of how
563 * @copyright 2011 The Open University
564 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
566 class test_recordset extends moodle_recordset {
571 * @param $table as for {@link testing_db_record_builder::build_db_records()}
572 * but does not need a unique first column.
574 public function __construct(array $table) {
575 $columns = array_shift($table);
576 $this->records = array();
577 foreach ($table as $row) {
578 if (count($row) != count($columns)) {
579 throw new coding_exception("Row contains the wrong number of fields.");
582 foreach ($columns as $i => $name) {
583 $rec[$name] = $row[$i];
585 $this->records[] = $rec;
587 reset($this->records);
590 public function __destruct() {
594 public function current() {
595 return (object) current($this->records);
598 public function key() {
599 if (is_null(key($this->records))) {
602 $current = current($this->records);
603 return reset($current);
606 public function next() {
607 next($this->records);
610 public function valid() {
611 return !is_null(key($this->records));
614 public function close() {
615 $this->records = null;
621 * This class lets you write unit tests that access a separate set of test
622 * tables with a different prefix. Only those tables you explicitly ask to
623 * be created will be.
625 * This class has failities for flipping $USER->id.
627 * The tear-down method for this class should automatically revert any changes
628 * you make during test set-up using the metods defined here. That is, it will
629 * drop tables for you automatically and revert to the real $DB and $USER->id.
631 * @package moodlecore
632 * @subpackage simpletestex
633 * @copyright © 2006 The Open University
634 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
636 class UnitTestCaseUsingDatabase extends UnitTestCase {
639 private $realuserid = null;
640 private $tables = array();
645 public function __construct($label = false) {
648 // Complain if we get this far and $CFG->unittestprefix is not set.
649 if (empty($CFG->unittestprefix)) {
650 throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
653 // Only do this after the above text.
654 parent::UnitTestCase($label);
656 // Create the test DB instance.
658 $this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
659 $this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
661 // Set up test config
662 $this->testcfg = (object)array(
663 'testcfg' => true, // Marker that this is a test config
664 'libdir' => $CFG->libdir, // Must use real one so require_once works
665 'dirroot' => $CFG->dirroot, // Must use real one
666 'dataroot' => $CFG->dataroot, // Use real one for now (maybe this should change?)
667 'ostype' => $CFG->ostype, // Real one
668 'wwwroot' => 'http://www.example.org', // Use fixed url
669 'siteadmins' => '0', // No admins
670 'siteguest' => '0' // No guest
672 $this->realcfg = $CFG;
676 * Switch to using the test database for all queries until further notice.
678 protected function switch_to_test_db() {
680 if ($DB === $this->testdb) {
681 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);
687 * Revert to using the test database for all future queries.
689 protected function revert_to_real_db() {
691 if ($DB !== $this->testdb) {
692 debugging('revert_to_real_db called when the test DB was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
698 * Switch to using the test $CFG for all queries until further notice.
700 protected function switch_to_test_cfg() {
702 if (isset($CFG->testcfg)) {
703 debugging('switch_to_test_cfg called when the test CFG was already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
705 $CFG = $this->testcfg;
709 * Revert to using the real $CFG for all future queries.
711 protected function revert_to_real_cfg() {
713 if (!isset($CFG->testcfg)) {
714 debugging('revert_to_real_cfg called when the test CFG was not already selected. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
716 $CFG = $this->realcfg;
720 * Switch $USER->id to a test value.
722 * It might be worth making this method do more robuse $USER switching in future,
723 * however, this is sufficient for my needs at present.
725 protected function switch_global_user_id($userid) {
727 if (!is_null($this->realuserid)) {
728 debugging('switch_global_user_id called when $USER->id was already switched to a different value. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
730 $this->realuserid = $USER->id;
736 * Revert $USER->id to the real value.
738 protected function revert_global_user_id() {
740 if (is_null($this->realuserid)) {
741 debugging('revert_global_user_id called without switch_global_user_id having been called first. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
743 $USER->id = $this->realuserid;
744 $this->realuserid = null;
749 * Check that the user has not forgotten to clean anything up, and if they
750 * have, display a rude message and clean it up for them.
752 private function automatic_clean_up() {
756 // Drop any test tables that were created.
757 foreach ($this->tables as $tablename => $notused) {
758 $this->drop_test_table($tablename);
761 // Switch back to the real DB if necessary.
762 if ($DB !== $this->realdb) {
763 $this->revert_to_real_db();
767 // Switch back to the real CFG if necessary.
768 if (isset($CFG->testcfg)) {
769 $this->revert_to_real_cfg();
773 // revert_global_user_id if necessary.
774 if (!is_null($this->realuserid)) {
775 $this->revert_global_user_id();
780 accesslib_clear_all_caches_for_unit_testing();
782 get_fast_modinfo($course);
786 public function tearDown() {
787 $this->automatic_clean_up();
791 public function __destruct() {
792 // Should not be necessary thanks to tearDown, but no harm in belt and braces.
793 $this->automatic_clean_up();
797 * Create a test table just like a real one, getting getting the definition from
798 * the specified install.xml file.
799 * @param string $tablename the name of the test table.
800 * @param string $installxmlfile the install.xml file in which this table is defined.
801 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
802 * so you need only specify, for example, 'mod/quiz'.
804 protected function create_test_table($tablename, $installxmlfile) {
806 $dbman = $this->testdb->get_manager();
807 if (isset($this->tables[$tablename])) {
808 debugging('You are attempting to create test table ' . $tablename . ' again. It already exists. Please review your code immediately.', DEBUG_DEVELOPER);
811 if ($dbman->table_exists($tablename)) {
812 debugging('This table ' . $tablename . ' already exists from a previous execution. If the error persists you will need to review your code to ensure it is being created only once.', DEBUG_DEVELOPER);
813 $dbman->drop_table(new xmldb_table($tablename));
815 $dbman->install_one_table_from_xmldb_file($CFG->dirroot . '/' . $installxmlfile . '/db/install.xml', $tablename, true); // with structure cache enabled!
816 $this->tables[$tablename] = 1;
820 * Convenience method for calling create_test_table repeatedly.
821 * @param array $tablenames an array of table names.
822 * @param string $installxmlfile the install.xml file in which this table is defined.
823 * $CFG->dirroot . '/' will be prepended, and '/db/install.xml' appended,
824 * so you need only specify, for example, 'mod/quiz'.
826 protected function create_test_tables($tablenames, $installxmlfile) {
827 foreach ($tablenames as $tablename) {
828 $this->create_test_table($tablename, $installxmlfile);
834 * @param $tablename the name of the test table.
836 protected function drop_test_table($tablename) {
837 if (!isset($this->tables[$tablename])) {
838 debugging('You are attempting to drop test table ' . $tablename . ' but it does not exist. Please review your code immediately.', DEBUG_DEVELOPER);
841 $dbman = $this->testdb->get_manager();
842 $table = new xmldb_table($tablename);
843 $dbman->drop_table($table);
844 unset($this->tables[$tablename]);
848 * Convenience method for calling drop_test_table repeatedly.
849 * @param array $tablenames an array of table names.
851 protected function drop_test_tables($tablenames) {
852 foreach ($tablenames as $tablename) {
853 $this->drop_test_table($tablename);
858 * Load a table with some rows of data. A typical call would look like:
860 * $config = $this->load_test_data('config_plugins',
861 * array('plugin', 'name', 'value'), array(
862 * array('frog', 'numlegs', 2),
863 * array('frog', 'sound', 'croak'),
864 * array('frog', 'action', 'jump'),
867 * @param string $table the table name.
868 * @param array $cols the columns to fill.
869 * @param array $data the data to load.
870 * @return array $objects corresponding to $data.
872 protected function load_test_data($table, array $cols, array $data) {
874 foreach ($data as $rowid => $row) {
876 foreach ($cols as $key => $colname) {
877 $obj->$colname = $row[$key];
879 $obj->id = $this->testdb->insert_record($table, $obj);
880 $results[$rowid] = $obj;
886 * Clean up data loaded with load_test_data. The call corresponding to the
887 * example load above would be:
889 * $this->delete_test_data('config_plugins', $config);
891 * @param string $table the table name.
892 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
894 protected function delete_test_data($table, array $rows) {
896 foreach ($rows as $row) {
899 $this->testdb->delete_records_list($table, 'id', $ids);
905 * @package moodlecore
906 * @subpackage simpletestex
907 * @copyright © 2006 The Open University
908 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
910 class FakeDBUnitTestCase extends UnitTestCase {
911 public $tables = array();
917 * In the constructor, record the max(id) of each test table into a csv file.
918 * If this file already exists, it means that a previous run of unit tests
919 * did not complete, and has left data undeleted in the DB. This data is then
920 * deleted and the file is retained. Otherwise it is created.
922 * throws moodle_exception if CSV file cannot be created
924 public function __construct($label = false) {
927 if (empty($CFG->unittestprefix)) {
931 parent::UnitTestCase($label);
932 // MDL-16483 Get PKs and save data to text file
934 $this->pkfile = $CFG->dataroot.'/testtablespks.csv';
937 UnitTestDB::instantiate();
939 $tables = $DB->get_tables();
941 // The file exists, so use it to truncate tables (tests aborted before test data could be removed)
942 if (file_exists($this->pkfile)) {
943 $this->truncate_test_tables($this->get_table_data($this->pkfile));
945 } else { // Create the file
948 foreach ($tables as $table) {
949 if ($table != 'sessions') {
950 if (!$max_id = $DB->get_field_sql("SELECT MAX(id) FROM {$CFG->unittestprefix}{$table}")) {
953 $tabledata .= "$table, $max_id\n";
956 if (!file_put_contents($this->pkfile, $tabledata)) {
958 $a->filename = $this->pkfile;
959 throw new moodle_exception('testtablescsvfileunwritable', 'tool_unittest', '', $a);
965 * Given an array of tables and their max id, truncates all test table records whose id is higher than the ones in the $tabledata array.
966 * @param array $tabledata
968 private function truncate_test_tables($tabledata) {
971 if (empty($CFG->unittestprefix)) {
975 $tables = $DB->get_tables();
977 foreach ($tables as $table) {
978 if ($table != 'sessions' && isset($tabledata[$table])) {
979 // $DB->delete_records_select($table, "id > ?", array($tabledata[$table]));
985 * Given a filename, opens it and parses the csv contained therein. It expects two fields per line:
989 * throws moodle_exception if file doesn't exist
991 * @param string $filename
993 public function get_table_data($filename) {
996 if (empty($CFG->unittestprefix)) {
1000 if (file_exists($this->pkfile)) {
1001 $handle = fopen($this->pkfile, 'r');
1002 $tabledata = array();
1004 while (($data = fgetcsv($handle, 1000, ",")) !== false) {
1005 $tabledata[$data[0]] = $data[1];
1009 $a = new stdClass();
1010 $a->filename = $this->pkfile;
1011 throw new moodle_exception('testtablescsvfilemissing', 'tool_unittest', '', $a);
1017 * Method called before each test method. Replaces the real $DB with the one configured for unit tests (different prefix, $CFG->unittestprefix).
1018 * Also detects if this config setting is properly set, and if the user table exists.
1019 * @todo Improve detection of incorrectly built DB test tables (e.g. detect version discrepancy and offer to upgrade/rebuild)
1021 public function setUp() {
1024 if (empty($CFG->unittestprefix)) {
1034 * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one.
1036 public function tearDown() {
1039 if (empty($CFG->unittestprefix)) {
1050 if (ob_get_length() > 0) {
1056 * This will execute once all the tests have been run. It should delete the text file holding info about database contents prior to the tests
1057 * It should also detect if data is missing from the original tables.
1059 public function __destruct() {
1062 if (empty($CFG->unittestprefix)) {
1068 UnitTestDB::restore();
1069 fulldelete($this->pkfile);
1073 * Load a table with some rows of data. A typical call would look like:
1075 * $config = $this->load_test_data('config_plugins',
1076 * array('plugin', 'name', 'value'), array(
1077 * array('frog', 'numlegs', 2),
1078 * array('frog', 'sound', 'croak'),
1079 * array('frog', 'action', 'jump'),
1082 * @param string $table the table name.
1083 * @param array $cols the columns to fill.
1084 * @param array $data the data to load.
1085 * @return array $objects corresponding to $data.
1087 public function load_test_data($table, array $cols, array $data) {
1090 if (empty($CFG->unittestprefix)) {
1095 foreach ($data as $rowid => $row) {
1096 $obj = new stdClass;
1097 foreach ($cols as $key => $colname) {
1098 $obj->$colname = $row[$key];
1100 $obj->id = $DB->insert_record($table, $obj);
1101 $results[$rowid] = $obj;
1107 * Clean up data loaded with load_test_data. The call corresponding to the
1108 * example load above would be:
1110 * $this->delete_test_data('config_plugins', $config);
1112 * @param string $table the table name.
1113 * @param array $rows the rows to delete. Actually, only $rows[$key]->id is used.
1115 public function delete_test_data($table, array $rows) {
1118 if (empty($CFG->unittestprefix)) {
1123 foreach ($rows as $row) {
1126 $DB->delete_records_list($table, 'id', $ids);
1131 * This is a Database Engine proxy class: It replaces the global object $DB with itself through a call to the
1132 * static instantiate() method, and restores the original global $DB through restore().
1133 * Internally, it routes all calls to $DB to a real instance of the database engine (aggregated as a member variable),
1134 * except those that are defined in this proxy class. This makes it possible to add extra code to the database engine
1135 * without subclassing it.
1137 * @package moodlecore
1138 * @subpackage simpletestex
1139 * @copyright © 2006 The Open University
1140 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1144 private static $real_db;
1146 public $table_data = array();
1149 * Call this statically to connect to the DB using the unittest prefix, instantiate
1150 * the unit test db, store it as a member variable, instantiate $this and use it as the new global $DB.
1152 public static function instantiate() {
1154 UnitTestDB::$real_db = clone($DB);
1155 if (empty($CFG->unittestprefix)) {
1156 print_error("prefixnotset", 'tool_unittest');
1159 if (empty(UnitTestDB::$DB)) {
1160 UnitTestDB::$DB = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
1161 UnitTestDB::$DB->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
1164 $manager = UnitTestDB::$DB->get_manager();
1166 if (!$manager->table_exists('user')) {
1167 print_error('tablesnotsetup', 'tool_unittest');
1170 $DB = new UnitTestDB();
1173 public function __call($method, $args) {
1174 // Set args to null if they don't exist (up to 10 args should do)
1175 if (!method_exists($this, $method)) {
1176 return call_user_func_array(array(UnitTestDB::$DB, $method), $args);
1178 call_user_func_array(array($this, $method), $args);
1182 public function __get($variable) {
1183 return UnitTestDB::$DB->$variable;
1186 public function __set($variable, $value) {
1187 UnitTestDB::$DB->$variable = $value;
1190 public function __isset($variable) {
1191 return isset(UnitTestDB::$DB->$variable);
1194 public function __unset($variable) {
1195 unset(UnitTestDB::$DB->$variable);
1199 * Overriding insert_record to keep track of the ids inserted during unit tests, so that they can be deleted afterwards
1201 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1203 $id = UnitTestDB::$DB->insert_record($table, $dataobject, $returnid, $bulk);
1204 $this->table_data[$table][] = $id;
1209 * Overriding update_record: If we are updating a record that was NOT inserted by unit tests,
1210 * throw an exception and cancel update.
1212 * throws moodle_exception If trying to update a record not inserted by unit tests.
1214 public function update_record($table, $dataobject, $bulk=false) {
1216 if ((empty($this->table_data[$table]) || !in_array($dataobject->id, $this->table_data[$table])) && !($table == 'course_categories' && $dataobject->id == 1)) {
1217 // return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1218 $a = new stdClass();
1219 $a->id = $dataobject->id;
1221 throw new moodle_exception('updatingnoninsertedrecord', 'tool_unittest', '', $a);
1223 return UnitTestDB::$DB->update_record($table, $dataobject, $bulk);
1228 * Overriding delete_record: If we are deleting a record that was NOT inserted by unit tests,
1229 * throw an exception and cancel delete.
1231 * throws moodle_exception If trying to delete a record not inserted by unit tests.
1233 public function delete_records($table, array $conditions=array()) {
1235 $tables_to_ignore = array('context_temp');
1237 $a = new stdClass();
1240 // Get ids matching conditions
1241 if (!$ids_to_delete = $DB->get_field($table, 'id', $conditions)) {
1242 return UnitTestDB::$DB->delete_records($table, $conditions);
1245 $proceed_with_delete = true;
1247 if (!is_array($ids_to_delete)) {
1248 $ids_to_delete = array($ids_to_delete);
1251 foreach ($ids_to_delete as $id) {
1252 if (!in_array($table, $tables_to_ignore) && (empty($this->table_data[$table]) || !in_array($id, $this->table_data[$table]))) {
1253 $proceed_with_delete = false;
1259 if ($proceed_with_delete) {
1260 return UnitTestDB::$DB->delete_records($table, $conditions);
1262 throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
1267 * Overriding delete_records_select: If we are deleting a record that was NOT inserted by unit tests,
1268 * throw an exception and cancel delete.
1270 * throws moodle_exception If trying to delete a record not inserted by unit tests.
1272 public function delete_records_select($table, $select, array $params=null) {
1274 $a = new stdClass();
1277 // Get ids matching conditions
1278 if (!$ids_to_delete = $DB->get_field_select($table, 'id', $select, $params)) {
1279 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1282 $proceed_with_delete = true;
1284 foreach ($ids_to_delete as $id) {
1285 if (!in_array($id, $this->table_data[$table])) {
1286 $proceed_with_delete = false;
1292 if ($proceed_with_delete) {
1293 return UnitTestDB::$DB->delete_records_select($table, $select, $params);
1295 throw new moodle_exception('deletingnoninsertedrecord', 'tool_unittest', '', $a);
1300 * Removes from the test DB all the records that were inserted during unit tests,
1302 public function cleanup() {
1304 foreach ($this->table_data as $table => $ids) {
1305 foreach ($ids as $id) {
1306 $DB->delete_records($table, array('id' => $id));
1312 * Restores the global $DB object.
1314 public static function restore() {
1316 $DB = UnitTestDB::$real_db;
1319 public function get_field($table, $return, array $conditions) {
1320 if (!is_array($conditions)) {
1321 throw new coding_exception('$conditions is not an array.');
1323 return UnitTestDB::$DB->get_field($table, $return, $conditions);