[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 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. 8 // 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. 13 // 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/>. 16 17 /** 18 * This file contains helper classes for testing the question engine. 19 * 20 * @package moodlecore 21 * @subpackage questionengine 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 require_once (__DIR__ . '/../lib.php'); 31 require_once($CFG->dirroot . '/lib/phpunit/lib.php'); 32 33 34 /** 35 * Makes some protected methods of question_attempt public to facilitate testing. 36 * 37 * @copyright 2009 The Open University 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class testable_question_attempt extends question_attempt { 41 public function add_step(question_attempt_step $step) { 42 parent::add_step($step); 43 } 44 public function set_min_fraction($fraction) { 45 $this->minfraction = $fraction; 46 } 47 public function set_max_fraction($fraction) { 48 $this->maxfraction = $fraction; 49 } 50 public function set_behaviour(question_behaviour $behaviour) { 51 $this->behaviour = $behaviour; 52 } 53 } 54 55 56 /** 57 * Test subclass to allow access to some protected data so that the correct 58 * behaviour can be verified. 59 * 60 * @copyright 2012 The Open University 61 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 62 */ 63 class testable_question_engine_unit_of_work extends question_engine_unit_of_work { 64 public function get_modified() { 65 return $this->modified; 66 } 67 68 public function get_attempts_added() { 69 return $this->attemptsadded; 70 } 71 72 public function get_attempts_modified() { 73 return $this->attemptsmodified; 74 } 75 76 public function get_steps_added() { 77 return $this->stepsadded; 78 } 79 80 public function get_steps_modified() { 81 return $this->stepsmodified; 82 } 83 84 public function get_steps_deleted() { 85 return $this->stepsdeleted; 86 } 87 88 public function get_metadata_added() { 89 return $this->metadataadded; 90 } 91 92 public function get_metadata_modified() { 93 return $this->metadatamodified; 94 } 95 } 96 97 98 /** 99 * Base class for question type test helpers. 100 * 101 * @copyright 2011 The Open University 102 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 103 */ 104 abstract class question_test_helper { 105 /** 106 * @return array of example question names that can be passed as the $which 107 * argument of {@link test_question_maker::make_question} when $qtype is 108 * this question type. 109 */ 110 abstract public function get_test_questions(); 111 112 /** 113 * Set up a form to create a question in $cat. This method also sets cat and contextid on $questiondata object. 114 * @param object $cat the category 115 * @param object $questiondata form initialisation requires question data. 116 * @return moodleform 117 */ 118 public static function get_question_editing_form($cat, $questiondata) { 119 $catcontext = context::instance_by_id($cat->contextid, MUST_EXIST); 120 $contexts = new question_edit_contexts($catcontext); 121 $dataforformconstructor = new stdClass(); 122 $dataforformconstructor->qtype = $questiondata->qtype; 123 $dataforformconstructor->contextid = $questiondata->contextid = $catcontext->id; 124 $dataforformconstructor->category = $questiondata->category = $cat->id; 125 $dataforformconstructor->formoptions = new stdClass(); 126 $dataforformconstructor->formoptions->canmove = true; 127 $dataforformconstructor->formoptions->cansaveasnew = true; 128 $dataforformconstructor->formoptions->canedit = true; 129 $dataforformconstructor->formoptions->repeatelements = true; 130 $qtype = question_bank::get_qtype($questiondata->qtype); 131 return $qtype->create_editing_form('question.php', $dataforformconstructor, $cat, $contexts, true); 132 } 133 } 134 135 136 /** 137 * This class creates questions of various types, which can then be used when 138 * testing. 139 * 140 * @copyright 2009 The Open University 141 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 142 */ 143 class test_question_maker { 144 const STANDARD_OVERALL_CORRECT_FEEDBACK = 'Well done!'; 145 const STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK = 146 'Parts, but only parts, of your response are correct.'; 147 const STANDARD_OVERALL_INCORRECT_FEEDBACK = 'That is not right at all.'; 148 149 /** @var array qtype => qtype test helper class. */ 150 protected static $testhelpers = array(); 151 152 /** 153 * Just make a question_attempt at a question. Useful for unit tests that 154 * need to pass a $qa to methods that call format_text. Probably not safe 155 * to use for anything beyond that. 156 * @param question_definition $question a question. 157 * @param number $maxmark the max mark to set. 158 * @return question_attempt the question attempt. 159 */ 160 public static function get_a_qa($question, $maxmark = 3) { 161 return new question_attempt($question, 13, null, $maxmark); 162 } 163 164 /** 165 * Initialise the common fields of a question of any type. 166 */ 167 public static function initialise_a_question($q) { 168 global $USER; 169 170 $q->id = 0; 171 $q->category = 0; 172 $q->parent = 0; 173 $q->questiontextformat = FORMAT_HTML; 174 $q->generalfeedbackformat = FORMAT_HTML; 175 $q->defaultmark = 1; 176 $q->penalty = 0.3333333; 177 $q->length = 1; 178 $q->stamp = make_unique_id_code(); 179 $q->version = make_unique_id_code(); 180 $q->hidden = 0; 181 $q->timecreated = time(); 182 $q->timemodified = time(); 183 $q->createdby = $USER->id; 184 $q->modifiedby = $USER->id; 185 } 186 187 public static function initialise_question_data($qdata) { 188 global $USER; 189 190 $qdata->id = 0; 191 $qdata->category = 0; 192 $qdata->contextid = 0; 193 $qdata->parent = 0; 194 $qdata->questiontextformat = FORMAT_HTML; 195 $qdata->generalfeedbackformat = FORMAT_HTML; 196 $qdata->defaultmark = 1; 197 $qdata->penalty = 0.3333333; 198 $qdata->length = 1; 199 $qdata->stamp = make_unique_id_code(); 200 $qdata->version = make_unique_id_code(); 201 $qdata->hidden = 0; 202 $qdata->timecreated = time(); 203 $qdata->timemodified = time(); 204 $qdata->createdby = $USER->id; 205 $qdata->modifiedby = $USER->id; 206 $qdata->hints = array(); 207 } 208 209 /** 210 * Get the test helper class for a particular question type. 211 * @param $qtype the question type name, e.g. 'multichoice'. 212 * @return question_test_helper the test helper class. 213 */ 214 public static function get_test_helper($qtype) { 215 global $CFG; 216 217 if (array_key_exists($qtype, self::$testhelpers)) { 218 return self::$testhelpers[$qtype]; 219 } 220 221 $file = core_component::get_plugin_directory('qtype', $qtype) . '/tests/helper.php'; 222 if (!is_readable($file)) { 223 throw new coding_exception('Question type ' . $qtype . 224 ' does not have test helper code.'); 225 } 226 include_once($file); 227 228 $class = 'qtype_' . $qtype . '_test_helper'; 229 if (!class_exists($class)) { 230 throw new coding_exception('Class ' . $class . ' is not defined in ' . $file); 231 } 232 233 self::$testhelpers[$qtype] = new $class(); 234 return self::$testhelpers[$qtype]; 235 } 236 237 /** 238 * Call a method on a qtype_{$qtype}_test_helper class and return the result. 239 * 240 * @param string $methodtemplate e.g. 'make_{qtype}_question_{which}'; 241 * @param string $qtype the question type to get a test question for. 242 * @param string $which one of the names returned by the get_test_questions 243 * method of the relevant qtype_{$qtype}_test_helper class. 244 * @param unknown_type $which 245 */ 246 protected static function call_question_helper_method($methodtemplate, $qtype, $which = null) { 247 $helper = self::get_test_helper($qtype); 248 249 $available = $helper->get_test_questions(); 250 251 if (is_null($which)) { 252 $which = reset($available); 253 } else if (!in_array($which, $available)) { 254 throw new coding_exception('Example question ' . $which . ' of type ' . 255 $qtype . ' does not exist.'); 256 } 257 258 $method = str_replace(array('{qtype}', '{which}'), 259 array($qtype, $which), $methodtemplate); 260 261 if (!method_exists($helper, $method)) { 262 throw new coding_exception('Method ' . $method . ' does not exist on the ' . 263 $qtype . ' question type test helper class.'); 264 } 265 266 return $helper->$method(); 267 } 268 269 /** 270 * Question types can provide a number of test question defintions. 271 * They do this by creating a qtype_{$qtype}_test_helper class that extends 272 * question_test_helper. The get_test_questions method returns the list of 273 * test questions available for this question type. 274 * 275 * @param string $qtype the question type to get a test question for. 276 * @param string $which one of the names returned by the get_test_questions 277 * method of the relevant qtype_{$qtype}_test_helper class. 278 * @return question_definition the requested question object. 279 */ 280 public static function make_question($qtype, $which = null) { 281 return self::call_question_helper_method('make_{qtype}_question_{which}', 282 $qtype, $which); 283 } 284 285 /** 286 * Like {@link make_question()} but returns the datastructure from 287 * get_question_options instead of the question_definition object. 288 * 289 * @param string $qtype the question type to get a test question for. 290 * @param string $which one of the names returned by the get_test_questions 291 * method of the relevant qtype_{$qtype}_test_helper class. 292 * @return stdClass the requested question object. 293 */ 294 public static function get_question_data($qtype, $which = null) { 295 return self::call_question_helper_method('get_{qtype}_question_data_{which}', 296 $qtype, $which); 297 } 298 299 /** 300 * Like {@link make_question()} but returns the data what would be saved from 301 * the question editing form instead of the question_definition object. 302 * 303 * @param string $qtype the question type to get a test question for. 304 * @param string $which one of the names returned by the get_test_questions 305 * method of the relevant qtype_{$qtype}_test_helper class. 306 * @return stdClass the requested question object. 307 */ 308 public static function get_question_form_data($qtype, $which = null) { 309 return self::call_question_helper_method('get_{qtype}_question_form_data_{which}', 310 $qtype, $which); 311 } 312 313 /** 314 * Makes a multichoice question with choices 'A', 'B' and 'C' shuffled. 'A' 315 * is correct, defaultmark 1. 316 * @return qtype_multichoice_single_question 317 */ 318 public static function make_a_multichoice_single_question() { 319 question_bank::load_question_definition_classes('multichoice'); 320 $mc = new qtype_multichoice_single_question(); 321 self::initialise_a_question($mc); 322 $mc->name = 'Multi-choice question, single response'; 323 $mc->questiontext = 'The answer is A.'; 324 $mc->generalfeedback = 'You should have selected A.'; 325 $mc->qtype = question_bank::get_qtype('multichoice'); 326 327 $mc->shuffleanswers = 1; 328 $mc->answernumbering = 'abc'; 329 330 $mc->answers = array( 331 13 => new question_answer(13, 'A', 1, 'A is right', FORMAT_HTML), 332 14 => new question_answer(14, 'B', -0.3333333, 'B is wrong', FORMAT_HTML), 333 15 => new question_answer(15, 'C', -0.3333333, 'C is wrong', FORMAT_HTML), 334 ); 335 336 return $mc; 337 } 338 339 /** 340 * Makes a multichoice question with choices 'A', 'B', 'C' and 'D' shuffled. 341 * 'A' and 'C' is correct, defaultmark 1. 342 * @return qtype_multichoice_multi_question 343 */ 344 public static function make_a_multichoice_multi_question() { 345 question_bank::load_question_definition_classes('multichoice'); 346 $mc = new qtype_multichoice_multi_question(); 347 self::initialise_a_question($mc); 348 $mc->name = 'Multi-choice question, multiple response'; 349 $mc->questiontext = 'The answer is A and C.'; 350 $mc->generalfeedback = 'You should have selected A and C.'; 351 $mc->qtype = question_bank::get_qtype('multichoice'); 352 353 $mc->shuffleanswers = 1; 354 $mc->answernumbering = 'abc'; 355 356 self::set_standard_combined_feedback_fields($mc); 357 358 $mc->answers = array( 359 13 => new question_answer(13, 'A', 0.5, 'A is part of the right answer', FORMAT_HTML), 360 14 => new question_answer(14, 'B', -1, 'B is wrong', FORMAT_HTML), 361 15 => new question_answer(15, 'C', 0.5, 'C is part of the right answer', FORMAT_HTML), 362 16 => new question_answer(16, 'D', -1, 'D is wrong', FORMAT_HTML), 363 ); 364 365 return $mc; 366 } 367 368 /** 369 * Makes a matching question to classify 'Dog', 'Frog', 'Toad' and 'Cat' as 370 * 'Mammal', 'Amphibian' or 'Insect'. 371 * defaultmark 1. Stems are shuffled by default. 372 * @return qtype_match_question 373 */ 374 public static function make_a_matching_question() { 375 question_bank::load_question_definition_classes('match'); 376 $match = new qtype_match_question(); 377 self::initialise_a_question($match); 378 $match->name = 'Matching question'; 379 $match->questiontext = 'Classify the animals.'; 380 $match->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.'; 381 $match->qtype = question_bank::get_qtype('match'); 382 383 $match->shufflestems = 1; 384 385 self::set_standard_combined_feedback_fields($match); 386 387 // Using unset to get 1-based arrays. 388 $match->stems = array('', 'Dog', 'Frog', 'Toad', 'Cat'); 389 $match->stemformat = array('', FORMAT_HTML, FORMAT_HTML, FORMAT_HTML, FORMAT_HTML); 390 $match->choices = array('', 'Mammal', 'Amphibian', 'Insect'); 391 $match->right = array('', 1, 2, 2, 1); 392 unset($match->stems[0]); 393 unset($match->stemformat[0]); 394 unset($match->choices[0]); 395 unset($match->right[0]); 396 397 return $match; 398 } 399 400 /** 401 * Makes a truefalse question with correct ansewer true, defaultmark 1. 402 * @return qtype_essay_question 403 */ 404 public static function make_an_essay_question() { 405 question_bank::load_question_definition_classes('essay'); 406 $essay = new qtype_essay_question(); 407 self::initialise_a_question($essay); 408 $essay->name = 'Essay question'; 409 $essay->questiontext = 'Write an essay.'; 410 $essay->generalfeedback = 'I hope you wrote an interesting essay.'; 411 $essay->penalty = 0; 412 $essay->qtype = question_bank::get_qtype('essay'); 413 414 $essay->responseformat = 'editor'; 415 $essay->responserequired = 1; 416 $essay->responsefieldlines = 15; 417 $essay->attachments = 0; 418 $essay->attachmentsrequired = 0; 419 $essay->responsetemplate = ''; 420 $essay->responsetemplateformat = FORMAT_MOODLE; 421 $essay->graderinfo = ''; 422 $essay->graderinfoformat = FORMAT_MOODLE; 423 424 return $essay; 425 } 426 427 /** 428 * Add some standard overall feedback to a question. You need to use these 429 * specific feedback strings for the corresponding contains_..._feedback 430 * methods in {@link qbehaviour_walkthrough_test_base} to works. 431 * @param question_definition $q the question to add the feedback to. 432 */ 433 public static function set_standard_combined_feedback_fields($q) { 434 $q->correctfeedback = self::STANDARD_OVERALL_CORRECT_FEEDBACK; 435 $q->correctfeedbackformat = FORMAT_HTML; 436 $q->partiallycorrectfeedback = self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK; 437 $q->partiallycorrectfeedbackformat = FORMAT_HTML; 438 $q->shownumcorrect = true; 439 $q->incorrectfeedback = self::STANDARD_OVERALL_INCORRECT_FEEDBACK; 440 $q->incorrectfeedbackformat = FORMAT_HTML; 441 } 442 443 /** 444 * Add some standard overall feedback to a question's form data. 445 */ 446 public static function set_standard_combined_feedback_form_data($form) { 447 $form->correctfeedback = array('text' => self::STANDARD_OVERALL_CORRECT_FEEDBACK, 448 'format' => FORMAT_HTML); 449 $form->partiallycorrectfeedback = array('text' => self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK, 450 'format' => FORMAT_HTML); 451 $form->shownumcorrect = true; 452 $form->incorrectfeedback = array('text' => self::STANDARD_OVERALL_INCORRECT_FEEDBACK, 453 'format' => FORMAT_HTML); 454 } 455 } 456 457 458 /** 459 * Helper for tests that need to simulate records loaded from the database. 460 * 461 * @copyright 2009 The Open University 462 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 463 */ 464 abstract class testing_db_record_builder { 465 public static function build_db_records(array $table) { 466 $columns = array_shift($table); 467 $records = array(); 468 foreach ($table as $row) { 469 if (count($row) != count($columns)) { 470 throw new coding_exception("Row contains the wrong number of fields."); 471 } 472 $rec = new stdClass(); 473 foreach ($columns as $i => $name) { 474 $rec->$name = $row[$i]; 475 } 476 $records[] = $rec; 477 } 478 return $records; 479 } 480 } 481 482 483 /** 484 * Helper base class for tests that need to simulate records loaded from the 485 * database. 486 * 487 * @copyright 2009 The Open University 488 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 489 */ 490 abstract class data_loading_method_test_base extends advanced_testcase { 491 public function build_db_records(array $table) { 492 return testing_db_record_builder::build_db_records($table); 493 } 494 } 495 496 497 abstract class question_testcase extends advanced_testcase { 498 499 public function assert($expectation, $compare, $notused = '') { 500 501 if (get_class($expectation) === 'question_pattern_expectation') { 502 $this->assertRegExp($expectation->pattern, $compare, 503 'Expected regex ' . $expectation->pattern . ' not found in ' . $compare); 504 return; 505 506 } else if (get_class($expectation) === 'question_no_pattern_expectation') { 507 $this->assertNotRegExp($expectation->pattern, $compare, 508 'Unexpected regex ' . $expectation->pattern . ' found in ' . $compare); 509 return; 510 511 } else if (get_class($expectation) === 'question_contains_tag_with_attributes') { 512 $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->expectedvalues), $compare, 513 'Looking for a ' . $expectation->tag . ' with attributes ' . html_writer::attributes($expectation->expectedvalues) . ' in ' . $compare); 514 foreach ($expectation->forbiddenvalues as $k=>$v) { 515 $attr = $expectation->expectedvalues; 516 $attr[$k] = $v; 517 $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, 518 $expectation->tag . ' had a ' . $k . ' attribute that should not be there in ' . $compare); 519 } 520 return; 521 522 } else if (get_class($expectation) === 'question_contains_tag_with_attribute') { 523 $attr = array($expectation->attribute=>$expectation->value); 524 $this->assertTag(array('tag'=>$expectation->tag, 'attributes'=>$attr), $compare, 525 'Looking for a ' . $expectation->tag . ' with attribute ' . html_writer::attributes($attr) . ' in ' . $compare); 526 return; 527 528 } else if (get_class($expectation) === 'question_does_not_contain_tag_with_attributes') { 529 $this->assertNotTag(array('tag'=>$expectation->tag, 'attributes'=>$expectation->attributes), $compare, 530 'Unexpected ' . $expectation->tag . ' with attributes ' . html_writer::attributes($expectation->attributes) . ' found in ' . $compare); 531 return; 532 533 } else if (get_class($expectation) === 'question_contains_select_expectation') { 534 $tag = array('tag'=>'select', 'attributes'=>array('name'=>$expectation->name), 535 'children'=>array('count'=>count($expectation->choices))); 536 if ($expectation->enabled === false) { 537 $tag['attributes']['disabled'] = 'disabled'; 538 } else if ($expectation->enabled === true) { 539 // TODO 540 } 541 foreach(array_keys($expectation->choices) as $value) { 542 if ($expectation->selected === $value) { 543 $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value, 'selected'=>'selected')); 544 } else { 545 $tag['child'] = array('tag'=>'option', 'attributes'=>array('value'=>$value)); 546 } 547 } 548 549 $this->assertTag($tag, $compare, 'expected select not found in ' . $compare); 550 return; 551 552 } else if (get_class($expectation) === 'question_check_specified_fields_expectation') { 553 $expect = (array)$expectation->expect; 554 $compare = (array)$compare; 555 foreach ($expect as $k=>$v) { 556 if (!array_key_exists($k, $compare)) { 557 $this->fail("Property {$k} does not exist"); 558 } 559 if ($v != $compare[$k]) { 560 $this->fail("Property {$k} is different"); 561 } 562 } 563 $this->assertTrue(true); 564 return; 565 566 } else if (get_class($expectation) === 'question_contains_tag_with_contents') { 567 $this->assertTag(array('tag'=>$expectation->tag, 'content'=>$expectation->content), $compare, 568 'Looking for a ' . $expectation->tag . ' with content ' . $expectation->content . ' in ' . $compare); 569 return; 570 } 571 572 throw new coding_exception('Unknown expectiontion:'.get_class($expectation)); 573 } 574 } 575 576 577 class question_contains_tag_with_contents { 578 public $tag; 579 public $content; 580 public $message; 581 582 public function __construct($tag, $content, $message = '') { 583 $this->tag = $tag; 584 $this->content = $content; 585 $this->message = $message; 586 } 587 588 } 589 590 class question_check_specified_fields_expectation { 591 public $expect; 592 public $message; 593 594 function __construct($expected, $message = '') { 595 $this->expect = $expected; 596 $this->message = $message; 597 } 598 } 599 600 601 class question_contains_select_expectation { 602 public $name; 603 public $choices; 604 public $selected; 605 public $enabled; 606 public $message; 607 608 public function __construct($name, $choices, $selected = null, $enabled = null, $message = '') { 609 $this->name = $name; 610 $this->choices = $choices; 611 $this->selected = $selected; 612 $this->enabled = $enabled; 613 $this->message = $message; 614 } 615 } 616 617 618 class question_does_not_contain_tag_with_attributes { 619 public $tag; 620 public $attributes; 621 public $message; 622 623 public function __construct($tag, $attributes, $message = '') { 624 $this->tag = $tag; 625 $this->attributes = $attributes; 626 $this->message = $message; 627 } 628 } 629 630 631 class question_contains_tag_with_attribute { 632 public $tag; 633 public $attribute; 634 public $value; 635 public $message; 636 637 public function __construct($tag, $attribute, $value, $message = '') { 638 $this->tag = $tag; 639 $this->attribute = $attribute; 640 $this->value = $value; 641 $this->message = $message; 642 } 643 } 644 645 646 class question_contains_tag_with_attributes { 647 public $tag; 648 public $expectedvalues = array(); 649 public $forbiddenvalues = array(); 650 public $message; 651 652 public function __construct($tag, $expectedvalues, $forbiddenvalues=array(), $message = '') { 653 $this->tag = $tag; 654 $this->expectedvalues = $expectedvalues; 655 $this->forbiddenvalues = $forbiddenvalues; 656 $this->message = $message; 657 } 658 } 659 660 661 class question_pattern_expectation { 662 public $pattern; 663 public $message; 664 665 public function __construct($pattern, $message = '') { 666 $this->pattern = $pattern; 667 $this->message = $message; 668 } 669 } 670 671 672 class question_no_pattern_expectation { 673 public $pattern; 674 public $message; 675 676 public function __construct($pattern, $message = '') { 677 $this->pattern = $pattern; 678 $this->message = $message; 679 } 680 } 681 682 683 /** 684 * Helper base class for tests that walk a question through a sequents of 685 * interactions under the control of a particular behaviour. 686 * 687 * @copyright 2009 The Open University 688 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 689 */ 690 abstract class qbehaviour_walkthrough_test_base extends question_testcase { 691 /** @var question_display_options */ 692 protected $displayoptions; 693 /** @var question_usage_by_activity */ 694 protected $quba; 695 /** @var integer */ 696 697 protected $slot; 698 /** 699 * @var string after {@link render()} has been called, this contains the 700 * display of the question in its current state. 701 */ 702 protected $currentoutput = ''; 703 704 protected function setUp() { 705 parent::setUp(); 706 $this->resetAfterTest(true); 707 708 $this->displayoptions = new question_display_options(); 709 $this->quba = question_engine::make_questions_usage_by_activity('unit_test', 710 context_system::instance()); 711 } 712 713 protected function tearDown() { 714 $this->displayoptions = null; 715 $this->quba = null; 716 parent::tearDown(); 717 } 718 719 protected function start_attempt_at_question($question, $preferredbehaviour, 720 $maxmark = null, $variant = 1) { 721 $this->quba->set_preferred_behaviour($preferredbehaviour); 722 $this->slot = $this->quba->add_question($question, $maxmark); 723 $this->quba->start_question($this->slot, $variant); 724 } 725 726 /** 727 * Convert an array of data destined for one question to the equivalent POST data. 728 * @param array $data the data for the quetsion. 729 * @return array the complete post data. 730 */ 731 protected function response_data_to_post($data) { 732 $prefix = $this->quba->get_field_prefix($this->slot); 733 $fulldata = array( 734 'slots' => $this->slot, 735 $prefix . ':sequencecheck' => $this->get_question_attempt()->get_sequence_check_count(), 736 ); 737 foreach ($data as $name => $value) { 738 $fulldata[$prefix . $name] = $value; 739 } 740 return $fulldata; 741 } 742 743 protected function process_submission($data) { 744 // Backwards compatibility. 745 reset($data); 746 if (count($data) == 1 && key($data) === '-finish') { 747 $this->finish(); 748 } 749 750 $this->quba->process_all_actions(time(), $this->response_data_to_post($data)); 751 } 752 753 protected function process_autosave($data) { 754 $this->quba->process_all_autosaves(null, $this->response_data_to_post($data)); 755 } 756 757 protected function finish() { 758 $this->quba->finish_all_questions(); 759 } 760 761 protected function manual_grade($comment, $mark, $commentformat = null) { 762 $this->quba->manual_grade($this->slot, $comment, $mark, $commentformat); 763 } 764 765 protected function save_quba(moodle_database $db = null) { 766 question_engine::save_questions_usage_by_activity($this->quba, $db); 767 } 768 769 protected function load_quba(moodle_database $db = null) { 770 $this->quba = question_engine::load_questions_usage_by_activity($this->quba->get_id(), $db); 771 } 772 773 protected function delete_quba() { 774 question_engine::delete_questions_usage_by_activity($this->quba->get_id()); 775 $this->quba = null; 776 } 777 778 protected function check_current_state($state) { 779 $this->assertEquals($state, $this->quba->get_question_state($this->slot), 780 'Questions is in the wrong state.'); 781 } 782 783 protected function check_current_mark($mark) { 784 if (is_null($mark)) { 785 $this->assertNull($this->quba->get_question_mark($this->slot)); 786 } else { 787 if ($mark == 0) { 788 // PHP will think a null mark and a mark of 0 are equal, 789 // so explicity check not null in this case. 790 $this->assertNotNull($this->quba->get_question_mark($this->slot)); 791 } 792 $this->assertEquals($mark, $this->quba->get_question_mark($this->slot), 793 'Expected mark and actual mark differ.', 0.000001); 794 } 795 } 796 797 /** 798 * Generate the HTML rendering of the question in its current state in 799 * $this->currentoutput so that it can be verified. 800 */ 801 protected function render() { 802 $this->currentoutput = $this->quba->render_question($this->slot, $this->displayoptions); 803 } 804 805 protected function check_output_contains_text_input($name, $value = null, $enabled = true) { 806 $attributes = array( 807 'type' => 'text', 808 'name' => $this->quba->get_field_prefix($this->slot) . $name, 809 ); 810 if (!is_null($value)) { 811 $attributes['value'] = $value; 812 } 813 if (!$enabled) { 814 $attributes['readonly'] = 'readonly'; 815 } 816 $matcher = $this->get_tag_matcher('input', $attributes); 817 $this->assertTag($matcher, $this->currentoutput, 818 'Looking for an input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 819 820 if ($enabled) { 821 $matcher['attributes']['readonly'] = 'readonly'; 822 $this->assertNotTag($matcher, $this->currentoutput, 823 'input with attributes ' . html_writer::attributes($attributes) . 824 ' should not be read-only in ' . $this->currentoutput); 825 } 826 } 827 828 protected function check_output_contains_text_input_with_class($name, $class = null) { 829 $attributes = array( 830 'type' => 'text', 831 'name' => $this->quba->get_field_prefix($this->slot) . $name, 832 ); 833 if (!is_null($class)) { 834 $attributes['class'] = 'regexp:/\b' . $class . '\b/'; 835 } 836 837 $matcher = $this->get_tag_matcher('input', $attributes); 838 $this->assertTag($matcher, $this->currentoutput, 839 'Looking for an input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 840 } 841 842 protected function check_output_does_not_contain_text_input_with_class($name, $class = null) { 843 $attributes = array( 844 'type' => 'text', 845 'name' => $this->quba->get_field_prefix($this->slot) . $name, 846 ); 847 if (!is_null($class)) { 848 $attributes['class'] = 'regexp:/\b' . $class . '\b/'; 849 } 850 851 $matcher = $this->get_tag_matcher('input', $attributes); 852 $this->assertNotTag($matcher, $this->currentoutput, 853 'Unexpected input with attributes ' . html_writer::attributes($attributes) . ' found in ' . $this->currentoutput); 854 } 855 856 protected function check_output_contains_hidden_input($name, $value) { 857 $attributes = array( 858 'type' => 'hidden', 859 'name' => $this->quba->get_field_prefix($this->slot) . $name, 860 'value' => $value, 861 ); 862 $this->assertTag($this->get_tag_matcher('input', $attributes), $this->currentoutput, 863 'Looking for a hidden input with attributes ' . html_writer::attributes($attributes) . ' in ' . $this->currentoutput); 864 } 865 866 protected function check_output_contains($string) { 867 $this->render(); 868 $this->assertContains($string, $this->currentoutput, 869 'Expected string ' . $string . ' not found in ' . $this->currentoutput); 870 } 871 872 protected function check_output_does_not_contain($string) { 873 $this->render(); 874 $this->assertNotContains($string, $this->currentoutput, 875 'String ' . $string . ' unexpectedly found in ' . $this->currentoutput); 876 } 877 878 protected function check_output_contains_lang_string($identifier, $component = '', $a = null) { 879 $this->check_output_contains(get_string($identifier, $component, $a)); 880 } 881 882 protected function get_tag_matcher($tag, $attributes) { 883 return array( 884 'tag' => $tag, 885 'attributes' => $attributes, 886 ); 887 } 888 889 /** 890 * @param $condition one or more Expectations. (users varargs). 891 */ 892 protected function check_current_output() { 893 $html = $this->quba->render_question($this->slot, $this->displayoptions); 894 foreach (func_get_args() as $condition) { 895 $this->assert($condition, $html); 896 } 897 } 898 899 protected function get_question_attempt() { 900 return $this->quba->get_question_attempt($this->slot); 901 } 902 903 protected function get_step_count() { 904 return $this->get_question_attempt()->get_num_steps(); 905 } 906 907 protected function check_step_count($expectednumsteps) { 908 $this->assertEquals($expectednumsteps, $this->get_step_count()); 909 } 910 911 protected function get_step($stepnum) { 912 return $this->get_question_attempt()->get_step($stepnum); 913 } 914 915 protected function get_contains_question_text_expectation($question) { 916 return new question_pattern_expectation('/' . preg_quote($question->questiontext, '/') . '/'); 917 } 918 919 protected function get_contains_general_feedback_expectation($question) { 920 return new question_pattern_expectation('/' . preg_quote($question->generalfeedback, '/') . '/'); 921 } 922 923 protected function get_does_not_contain_correctness_expectation() { 924 return new question_no_pattern_expectation('/class=\"correctness/'); 925 } 926 927 protected function get_contains_correct_expectation() { 928 return new question_pattern_expectation('/' . preg_quote(get_string('correct', 'question'), '/') . '/'); 929 } 930 931 protected function get_contains_partcorrect_expectation() { 932 return new question_pattern_expectation('/' . 933 preg_quote(get_string('partiallycorrect', 'question'), '/') . '/'); 934 } 935 936 protected function get_contains_incorrect_expectation() { 937 return new question_pattern_expectation('/' . preg_quote(get_string('incorrect', 'question'), '/') . '/'); 938 } 939 940 protected function get_contains_standard_correct_combined_feedback_expectation() { 941 return new question_pattern_expectation('/' . 942 preg_quote(test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK, '/') . '/'); 943 } 944 945 protected function get_contains_standard_partiallycorrect_combined_feedback_expectation() { 946 return new question_pattern_expectation('/' . 947 preg_quote(test_question_maker::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK, '/') . '/'); 948 } 949 950 protected function get_contains_standard_incorrect_combined_feedback_expectation() { 951 return new question_pattern_expectation('/' . 952 preg_quote(test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK, '/') . '/'); 953 } 954 955 protected function get_does_not_contain_feedback_expectation() { 956 return new question_no_pattern_expectation('/class="feedback"/'); 957 } 958 959 protected function get_does_not_contain_num_parts_correct() { 960 return new question_no_pattern_expectation('/class="numpartscorrect"/'); 961 } 962 963 protected function get_contains_num_parts_correct($num) { 964 $a = new stdClass(); 965 $a->num = $num; 966 return new question_pattern_expectation('/<div class="numpartscorrect">' . 967 preg_quote(get_string('yougotnright', 'question', $a), '/') . '/'); 968 } 969 970 protected function get_does_not_contain_specific_feedback_expectation() { 971 return new question_no_pattern_expectation('/class="specificfeedback"/'); 972 } 973 974 protected function get_contains_validation_error_expectation() { 975 return new question_contains_tag_with_attribute('div', 'class', 'validationerror'); 976 } 977 978 protected function get_does_not_contain_validation_error_expectation() { 979 return new question_no_pattern_expectation('/class="validationerror"/'); 980 } 981 982 protected function get_contains_mark_summary($mark) { 983 $a = new stdClass(); 984 $a->mark = format_float($mark, $this->displayoptions->markdp); 985 $a->max = format_float($this->quba->get_question_max_mark($this->slot), 986 $this->displayoptions->markdp); 987 return new question_pattern_expectation('/' . 988 preg_quote(get_string('markoutofmax', 'question', $a), '/') . '/'); 989 } 990 991 protected function get_contains_marked_out_of_summary() { 992 $max = format_float($this->quba->get_question_max_mark($this->slot), 993 $this->displayoptions->markdp); 994 return new question_pattern_expectation('/' . 995 preg_quote(get_string('markedoutofmax', 'question', $max), '/') . '/'); 996 } 997 998 protected function get_does_not_contain_mark_summary() { 999 return new question_no_pattern_expectation('/<div class="grade">/'); 1000 } 1001 1002 protected function get_contains_checkbox_expectation($baseattr, $enabled, $checked) { 1003 $expectedattributes = $baseattr; 1004 $forbiddenattributes = array(); 1005 $expectedattributes['type'] = 'checkbox'; 1006 if ($enabled === true) { 1007 $forbiddenattributes['disabled'] = 'disabled'; 1008 } else if ($enabled === false) { 1009 $expectedattributes['disabled'] = 'disabled'; 1010 } 1011 if ($checked === true) { 1012 $expectedattributes['checked'] = 'checked'; 1013 } else if ($checked === false) { 1014 $forbiddenattributes['checked'] = 'checked'; 1015 } 1016 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1017 } 1018 1019 protected function get_contains_mc_checkbox_expectation($index, $enabled = null, 1020 $checked = null) { 1021 return $this->get_contains_checkbox_expectation(array( 1022 'name' => $this->quba->get_field_prefix($this->slot) . $index, 1023 'value' => 1, 1024 ), $enabled, $checked); 1025 } 1026 1027 protected function get_contains_radio_expectation($baseattr, $enabled, $checked) { 1028 $expectedattributes = $baseattr; 1029 $forbiddenattributes = array(); 1030 $expectedattributes['type'] = 'radio'; 1031 if ($enabled === true) { 1032 $forbiddenattributes['disabled'] = 'disabled'; 1033 } else if ($enabled === false) { 1034 $expectedattributes['disabled'] = 'disabled'; 1035 } 1036 if ($checked === true) { 1037 $expectedattributes['checked'] = 'checked'; 1038 } else if ($checked === false) { 1039 $forbiddenattributes['checked'] = 'checked'; 1040 } 1041 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1042 } 1043 1044 protected function get_contains_mc_radio_expectation($index, $enabled = null, $checked = null) { 1045 return $this->get_contains_radio_expectation(array( 1046 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1047 'value' => $index, 1048 ), $enabled, $checked); 1049 } 1050 1051 protected function get_contains_hidden_expectation($name, $value = null) { 1052 $expectedattributes = array('type' => 'hidden', 'name' => s($name)); 1053 if (!is_null($value)) { 1054 $expectedattributes['value'] = s($value); 1055 } 1056 return new question_contains_tag_with_attributes('input', $expectedattributes); 1057 } 1058 1059 protected function get_does_not_contain_hidden_expectation($name, $value = null) { 1060 $expectedattributes = array('type' => 'hidden', 'name' => s($name)); 1061 if (!is_null($value)) { 1062 $expectedattributes['value'] = s($value); 1063 } 1064 return new question_does_not_contain_tag_with_attributes('input', $expectedattributes); 1065 } 1066 1067 protected function get_contains_tf_true_radio_expectation($enabled = null, $checked = null) { 1068 return $this->get_contains_radio_expectation(array( 1069 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1070 'value' => 1, 1071 ), $enabled, $checked); 1072 } 1073 1074 protected function get_contains_tf_false_radio_expectation($enabled = null, $checked = null) { 1075 return $this->get_contains_radio_expectation(array( 1076 'name' => $this->quba->get_field_prefix($this->slot) . 'answer', 1077 'value' => 0, 1078 ), $enabled, $checked); 1079 } 1080 1081 protected function get_contains_cbm_radio_expectation($certainty, $enabled = null, 1082 $checked = null) { 1083 return $this->get_contains_radio_expectation(array( 1084 'name' => $this->quba->get_field_prefix($this->slot) . '-certainty', 1085 'value' => $certainty, 1086 ), $enabled, $checked); 1087 } 1088 1089 protected function get_contains_button_expectation($name, $value = null, $enabled = null) { 1090 $expectedattributes = array( 1091 'type' => 'submit', 1092 'name' => $name, 1093 ); 1094 $forbiddenattributes = array(); 1095 if (!is_null($value)) { 1096 $expectedattributes['value'] = $value; 1097 } 1098 if ($enabled === true) { 1099 $forbiddenattributes['disabled'] = 'disabled'; 1100 } else if ($enabled === false) { 1101 $expectedattributes['disabled'] = 'disabled'; 1102 } 1103 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1104 } 1105 1106 /** 1107 * Returns an epectation that a string contains the HTML of a button with 1108 * name {question-attempt prefix}-submit, and eiter enabled or not. 1109 * @param bool $enabled if not null, check the enabled/disabled state of the button. True = enabled. 1110 * @return question_contains_tag_with_attributes an expectation for use with check_current_output. 1111 */ 1112 protected function get_contains_submit_button_expectation($enabled = null) { 1113 return $this->get_contains_button_expectation( 1114 $this->quba->get_field_prefix($this->slot) . '-submit', null, $enabled); 1115 } 1116 1117 /** 1118 * Returns an epectation that a string does not contain the HTML of a button with 1119 * name {question-attempt prefix}-submit. 1120 * @return question_contains_tag_with_attributes an expectation for use with check_current_output. 1121 */ 1122 protected function get_does_not_contain_submit_button_expectation() { 1123 return new question_no_pattern_expectation('/name="' . 1124 $this->quba->get_field_prefix($this->slot) . '-submit"/'); 1125 } 1126 1127 protected function get_tries_remaining_expectation($n) { 1128 return new question_pattern_expectation('/' . 1129 preg_quote(get_string('triesremaining', 'qbehaviour_interactive', $n), '/') . '/'); 1130 } 1131 1132 protected function get_invalid_answer_expectation() { 1133 return new question_pattern_expectation('/' . 1134 preg_quote(get_string('invalidanswer', 'question'), '/') . '/'); 1135 } 1136 1137 protected function get_contains_try_again_button_expectation($enabled = null) { 1138 $expectedattributes = array( 1139 'type' => 'submit', 1140 'name' => $this->quba->get_field_prefix($this->slot) . '-tryagain', 1141 ); 1142 $forbiddenattributes = array(); 1143 if ($enabled === true) { 1144 $forbiddenattributes['disabled'] = 'disabled'; 1145 } else if ($enabled === false) { 1146 $expectedattributes['disabled'] = 'disabled'; 1147 } 1148 return new question_contains_tag_with_attributes('input', $expectedattributes, $forbiddenattributes); 1149 } 1150 1151 protected function get_does_not_contain_try_again_button_expectation() { 1152 return new question_no_pattern_expectation('/name="' . 1153 $this->quba->get_field_prefix($this->slot) . '-tryagain"/'); 1154 } 1155 1156 protected function get_contains_select_expectation($name, $choices, 1157 $selected = null, $enabled = null) { 1158 $fullname = $this->quba->get_field_prefix($this->slot) . $name; 1159 return new question_contains_select_expectation($fullname, $choices, $selected, $enabled); 1160 } 1161 1162 protected function get_mc_right_answer_index($mc) { 1163 $order = $mc->get_order($this->get_question_attempt()); 1164 foreach ($order as $i => $ansid) { 1165 if ($mc->answers[$ansid]->fraction == 1) { 1166 return $i; 1167 } 1168 } 1169 $this->fail('This multiple choice question does not seem to have a right answer!'); 1170 } 1171 1172 protected function get_no_hint_visible_expectation() { 1173 return new question_no_pattern_expectation('/class="hint"/'); 1174 } 1175 1176 protected function get_contains_hint_expectation($hinttext) { 1177 // Does not currently verify hint text. 1178 return new question_contains_tag_with_attribute('div', 'class', 'hint'); 1179 } 1180 } 1181 1182 /** 1183 * Simple class that implements the {@link moodle_recordset} API based on an 1184 * array of test data. 1185 * 1186 * See the {@link question_attempt_step_db_test} class in 1187 * question/engine/tests/testquestionattemptstep.php for an example of how 1188 * this is used. 1189 * 1190 * @copyright 2011 The Open University 1191 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1192 */ 1193 class question_test_recordset extends moodle_recordset { 1194 protected $records; 1195 1196 /** 1197 * Constructor 1198 * @param $table as for {@link testing_db_record_builder::build_db_records()} 1199 * but does not need a unique first column. 1200 */ 1201 public function __construct(array $table) { 1202 $columns = array_shift($table); 1203 $this->records = array(); 1204 foreach ($table as $row) { 1205 if (count($row) != count($columns)) { 1206 throw new coding_exception("Row contains the wrong number of fields."); 1207 } 1208 $rec = array(); 1209 foreach ($columns as $i => $name) { 1210 $rec[$name] = $row[$i]; 1211 } 1212 $this->records[] = $rec; 1213 } 1214 reset($this->records); 1215 } 1216 1217 public function __destruct() { 1218 $this->close(); 1219 } 1220 1221 public function current() { 1222 return (object) current($this->records); 1223 } 1224 1225 public function key() { 1226 if (is_null(key($this->records))) { 1227 return false; 1228 } 1229 $current = current($this->records); 1230 return reset($current); 1231 } 1232 1233 public function next() { 1234 next($this->records); 1235 } 1236 1237 public function valid() { 1238 return !is_null(key($this->records)); 1239 } 1240 1241 public function close() { 1242 $this->records = null; 1243 } 1244 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |