Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

   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   * Unit tests for the multianswer question definition class.
  19   *
  20   * @package    qtype
  21   * @subpackage multianswer
  22   * @copyright  2011 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($CFG->dirroot . '/question/engine/tests/helpers.php');
  31  require_once($CFG->dirroot . '/question/type/multianswer/questiontype.php');
  32  require_once($CFG->dirroot . '/question/type/edit_question_form.php');
  33  require_once($CFG->dirroot . '/question/type/multianswer/edit_multianswer_form.php');
  34  
  35  
  36  /**
  37   * Unit tests for the multianswer question definition class.
  38   *
  39   * @copyright  2011 The Open University
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class qtype_multianswer_test extends advanced_testcase {
  43      /** @var qtype_multianswer instance of the question type class to test. */
  44      protected $qtype;
  45  
  46      protected function setUp(): void {
  47          $this->qtype = new qtype_multianswer();
  48      }
  49  
  50      protected function tearDown(): void {
  51          $this->qtype = null;
  52      }
  53  
  54      protected function get_test_question_data() {
  55          global $USER;
  56          $q = new stdClass();
  57          $q->id = 0;
  58          $q->name = 'Simple multianswer';
  59          $q->category = 0;
  60          $q->contextid = 0;
  61          $q->parent = 0;
  62          $q->questiontext =
  63                  'Complete this opening line of verse: "The {#1} and the {#2} went to sea".';
  64          $q->questiontextformat = FORMAT_HTML;
  65          $q->generalfeedback = 'Generalfeedback: It\'s from "The Owl and the Pussy-cat" by Lear: ' .
  66                  '"The owl and the pussycat went to see';
  67          $q->generalfeedbackformat = FORMAT_HTML;
  68          $q->defaultmark = 2;
  69          $q->penalty = 0.3333333;
  70          $q->length = 1;
  71          $q->stamp = make_unique_id_code();
  72          $q->version = make_unique_id_code();
  73          $q->hidden = 0;
  74          $q->timecreated = time();
  75          $q->timemodified = time();
  76          $q->createdby = $USER->id;
  77          $q->modifiedby = $USER->id;
  78  
  79          $sadata = new stdClass();
  80          $sadata->id = 1;
  81          $sadata->qtype = 'shortanswer';
  82          $sadata->defaultmark = 1;
  83          $sadata->options->usecase = true;
  84          $sadata->options->answers[1] = (object) array('answer' => 'Bow-wow', 'fraction' => 0);
  85          $sadata->options->answers[2] = (object) array('answer' => 'Wiggly worm', 'fraction' => 0);
  86          $sadata->options->answers[3] = (object) array('answer' => 'Pussy-cat', 'fraction' => 1);
  87  
  88          $mcdata = new stdClass();
  89          $mcdata->id = 1;
  90          $mcdata->qtype = 'multichoice';
  91          $mcdata->defaultmark = 1;
  92          $mcdata->options->single = true;
  93          $mcdata->options->answers[1] = (object) array('answer' => 'Dog', 'fraction' => 0);
  94          $mcdata->options->answers[2] = (object) array('answer' => 'Owl', 'fraction' => 1);
  95          $mcdata->options->answers[3] = (object) array('answer' => '*', 'fraction' => 0);
  96  
  97          $q->options->questions = array(
  98              1 => $sadata,
  99              2 => $mcdata,
 100          );
 101  
 102          return $q;
 103      }
 104  
 105      public function test_name() {
 106          $this->assertEquals($this->qtype->name(), 'multianswer');
 107      }
 108  
 109      public function test_can_analyse_responses() {
 110          $this->assertFalse($this->qtype->can_analyse_responses());
 111      }
 112  
 113      public function test_get_random_guess_score() {
 114          $q = test_question_maker::get_question_data('multianswer', 'twosubq');
 115          $this->assertEqualsWithDelta(0.1666667, $this->qtype->get_random_guess_score($q), 0.0000001);
 116      }
 117  
 118      public function test_load_question() {
 119          $this->resetAfterTest();
 120  
 121          $syscontext = context_system::instance();
 122          /** @var core_question_generator $generator */
 123          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 124          $category = $generator->create_question_category(['contextid' => $syscontext->id]);
 125  
 126          $fromform = test_question_maker::get_question_form_data('multianswer');
 127          $fromform->category = $category->id . ',' . $syscontext->id;
 128  
 129          $question = new stdClass();
 130          $question->category = $category->id;
 131          $question->qtype = 'multianswer';
 132          $question->createdby = 0;
 133  
 134          // Note, $question gets modified during save because of the way subquestions
 135          // are extracted.
 136          $question = $this->qtype->save_question($question, $fromform);
 137  
 138          $questiondata = question_bank::load_question_data($question->id);
 139  
 140          $this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
 141                  'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
 142                  'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified',
 143                  'createdby', 'modifiedby', 'idnumber', 'contextid', 'options', 'hints', 'categoryobject'],
 144                  array_keys(get_object_vars($questiondata)));
 145          $this->assertEquals($category->id, $questiondata->category);
 146          $this->assertEquals(0, $questiondata->parent);
 147          $this->assertEquals($fromform->name, $questiondata->name);
 148          $this->assertEquals($fromform->questiontext, $questiondata->questiontext);
 149          $this->assertEquals($fromform->questiontextformat, $questiondata->questiontextformat);
 150          $this->assertEquals($fromform->generalfeedback['text'], $questiondata->generalfeedback);
 151          $this->assertEquals($fromform->generalfeedback['format'], $questiondata->generalfeedbackformat);
 152          $this->assertEquals($fromform->defaultmark, $questiondata->defaultmark);
 153          $this->assertEquals(0, $questiondata->penalty);
 154          $this->assertEquals('multianswer', $questiondata->qtype);
 155          $this->assertEquals(1, $questiondata->length);
 156          $this->assertEquals(0, $questiondata->hidden);
 157          $this->assertEquals($question->createdby, $questiondata->createdby);
 158          $this->assertEquals($question->createdby, $questiondata->modifiedby);
 159          $this->assertEquals('', $questiondata->idnumber);
 160          $this->assertEquals($syscontext->id, $questiondata->contextid);
 161  
 162          // Build the expected hint base.
 163          $hintbase = [
 164              'questionid' => $questiondata->id,
 165              'shownumcorrect' => 0,
 166              'clearwrong' => 0,
 167              'options' => null];
 168          $expectedhints = [];
 169          foreach ($fromform->hint as $key => $value) {
 170              $hint = $hintbase + [
 171                  'hint' => $value['text'],
 172                  'hintformat' => $value['format'],
 173              ];
 174              $expectedhints[] = (object)$hint;
 175          }
 176          // Need to get rid of ids.
 177          $gothints = array_map(function($hint) {
 178              unset($hint->id);
 179              return $hint;
 180          }, $questiondata->hints);
 181          // Compare hints.
 182          $this->assertEquals($expectedhints, array_values($gothints));
 183  
 184          // Options.
 185          $this->assertEquals(['answers', 'questions'], array_keys(get_object_vars($questiondata->options)));
 186          $this->assertEquals(count($fromform->options->questions), count($questiondata->options->questions));
 187  
 188          // Option answers.
 189          $this->assertEquals([], $questiondata->options->answers);
 190  
 191          // Build the expected questions. We aren't going deeper to subquestion answers, options... that's another qtype job.
 192          $expectedquestions = [];
 193          foreach ($fromform->options->questions as $key => $value) {
 194              $question = [
 195                  'id' => $value->id,
 196                  'category' => $category->id,
 197                  'parent' => $questiondata->id,
 198                  'name' => $value->name,
 199                  'questiontext' => $value->questiontext,
 200                  'questiontextformat' => $value->questiontextformat,
 201                  'generalfeedback' => $value->generalfeedback,
 202                  'generalfeedbackformat' => $value->generalfeedbackformat,
 203                  'defaultmark' => (float) $value->defaultmark,
 204                  'penalty' => (float)$value->penalty,
 205                  'qtype' => $value->qtype,
 206                  'length' => $value->length,
 207                  'stamp' => $value->stamp,
 208                  'hidden' => 0,
 209                  'timecreated' => $value->timecreated,
 210                  'timemodified' => $value->timemodified,
 211                  'createdby' => $value->createdby,
 212                  'modifiedby' => $value->modifiedby,
 213              ];
 214              $expectedquestions[] = (object)$question;
 215          }
 216          // Need to get rid of (version, idnumber, options, hints, maxmark). They are missing @ fromform.
 217          $gotquestions = array_map(function($question) {
 218                  unset($question->version);
 219                  unset($question->idnumber);
 220                  unset($question->options);
 221                  unset($question->hints);
 222                  unset($question->maxmark);
 223                  return $question;
 224          }, $questiondata->options->questions);
 225          // Compare questions.
 226          $this->assertEquals($expectedquestions, array_values($gotquestions));
 227      }
 228  
 229      public function test_question_saving_twosubq() {
 230          $this->resetAfterTest(true);
 231          $this->setAdminUser();
 232  
 233          $questiondata = test_question_maker::get_question_data('multianswer');
 234          $formdata = test_question_maker::get_question_form_data('multianswer');
 235  
 236          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 237          $cat = $generator->create_question_category(array());
 238  
 239          $formdata->category = "{$cat->id},{$cat->contextid}";
 240          qtype_multianswer_edit_form::mock_submit((array)$formdata);
 241  
 242          $form = qtype_multianswer_test_helper::get_question_editing_form($cat, $questiondata);
 243  
 244          $this->assertTrue($form->is_validated());
 245  
 246          $fromform = $form->get_data();
 247  
 248          $returnedfromsave = $this->qtype->save_question($questiondata, $fromform);
 249          $actualquestionsdata = question_load_questions(array($returnedfromsave->id));
 250          $actualquestiondata = end($actualquestionsdata);
 251  
 252          foreach ($questiondata as $property => $value) {
 253              if (!in_array($property, array('id', 'version', 'timemodified', 'timecreated', 'options', 'hints', 'stamp'))) {
 254                  $this->assertEquals($value, $actualquestiondata->$property);
 255              }
 256          }
 257  
 258          foreach ($questiondata->options as $optionname => $value) {
 259              if ($optionname != 'questions') {
 260                  $this->assertEquals($value, $actualquestiondata->options->$optionname);
 261              }
 262          }
 263  
 264          foreach ($questiondata->hints as $hint) {
 265              $actualhint = array_shift($actualquestiondata->hints);
 266              foreach ($hint as $property => $value) {
 267                  if (!in_array($property, array('id', 'questionid', 'options'))) {
 268                      $this->assertEquals($value, $actualhint->$property);
 269                  }
 270              }
 271          }
 272  
 273          $this->assertObjectHasAttribute('questions', $actualquestiondata->options);
 274  
 275          $subqpropstoignore =
 276              array('id', 'category', 'parent', 'contextid', 'question', 'options', 'stamp', 'version', 'timemodified',
 277                  'timecreated');
 278          foreach ($questiondata->options->questions as $subqno => $subq) {
 279              $actualsubq = $actualquestiondata->options->questions[$subqno];
 280              foreach ($subq as $subqproperty => $subqvalue) {
 281                  if (!in_array($subqproperty, $subqpropstoignore)) {
 282                      $this->assertEquals($subqvalue, $actualsubq->$subqproperty);
 283                  }
 284              }
 285              foreach ($subq->options as $optionname => $value) {
 286                  if (!in_array($optionname, array('answers'))) {
 287                      $this->assertEquals($value, $actualsubq->options->$optionname);
 288                  }
 289              }
 290              foreach ($subq->options->answers as $answer) {
 291                  $actualanswer = array_shift($actualsubq->options->answers);
 292                  foreach ($answer as $ansproperty => $ansvalue) {
 293                      // These questions do not use 'answerformat', will ignore it.
 294                      if (!in_array($ansproperty, array('id', 'question', 'answerformat'))) {
 295                          $this->assertEquals($ansvalue, $actualanswer->$ansproperty);
 296                      }
 297                  }
 298              }
 299          }
 300      }
 301  
 302      /**
 303       *  Verify that the multiplechoice variants parameters are correctly interpreted from
 304       *  the question text
 305       */
 306      public function test_questiontext_extraction_of_multiplechoice_subquestions_variants() {
 307          $questiontext = array();
 308          $questiontext['format'] = FORMAT_HTML;
 309          $questiontext['itemid'] = '';
 310          $questiontext['text'] = '<p>Match the following cities with the correct state:</p>
 311              <ul>
 312              <li>1 San Francisco:{1:MULTICHOICE:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 313              <li>2 Tucson:{1:MC:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 314              <li>3 Los Angeles:{1:MULTICHOICE_S:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 315              <li>4 Phoenix:{1:MCS:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 316              <li>5 San Francisco:{1:MULTICHOICE_H:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 317              <li>6 Tucson:{1:MCH:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 318              <li>7 Los Angeles:{1:MULTICHOICE_HS:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 319              <li>8 Phoenix:{1:MCHS:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 320              <li>9 San Francisco:{1:MULTICHOICE_V:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 321              <li>10 Tucson:{1:MCV:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 322              <li>11 Los Angeles:{1:MULTICHOICE_VS:=California#OK~%33.33333%Ohio#Not really~Arizona#Wrong}</li>
 323              <li>12 Phoenix:{1:MCVS:%0%California#Wrong~%33,33333%Ohio#Not really~=Arizona#OK}</li>
 324              </ul>';
 325  
 326          $q = qtype_multianswer_extract_question($questiontext);
 327          foreach ($q->options->questions as $key => $sub) {
 328              $this->assertSame($sub->qtype, 'multichoice');
 329              if ($key == 1 || $key == 2 || $key == 5 || $key == 6 || $key == 9 || $key == 10) {
 330                  $this->assertSame($sub->shuffleanswers, 0);
 331              } else {
 332                  $this->assertSame($sub->shuffleanswers, 1);
 333              }
 334              if ($key == 1 || $key == 2 || $key == 3 || $key == 4) {
 335                  $this->assertSame($sub->layout, qtype_multichoice_base::LAYOUT_DROPDOWN);
 336              } else if ($key == 5 || $key == 6 || $key == 7 || $key == 8) {
 337                  $this->assertSame($sub->layout, qtype_multichoice_base::LAYOUT_HORIZONTAL);
 338              } else if ($key == 9 || $key == 10 || $key == 11 || $key == 12) {
 339                  $this->assertSame($sub->layout, qtype_multichoice_base::LAYOUT_VERTICAL);
 340              }
 341              foreach ($sub->feedback as $key => $feedback) {
 342                  if ($feedback['text'] === 'OK') {
 343                      $this->assertEquals(1, $sub->fraction[$key]);
 344                  } else if ($feedback['text'] === 'Wrong') {
 345                      $this->assertEquals(0, $sub->fraction[$key]);
 346                  } else {
 347                      $this->assertEquals('Not really', $feedback['text']);
 348                      $this->assertEquals(0.3333333, $sub->fraction[$key]);
 349                  }
 350              }
 351          }
 352      }
 353  }