Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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  namespace qtype_essay;
  18  
  19  use question_attempt_step;
  20  use question_display_options;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  global $CFG;
  25  require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  26  
  27  
  28  /**
  29   * Unit tests for the matching question definition class.
  30   *
  31   * @package qtype_essay
  32   * @copyright  2009 The Open University
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class question_test extends \advanced_testcase {
  36      public function test_get_question_summary() {
  37          $essay = \test_question_maker::make_an_essay_question();
  38          $essay->questiontext = 'Hello <img src="http://example.com/globe.png" alt="world" />';
  39          $this->assertEquals('Hello [world]', $essay->get_question_summary());
  40      }
  41  
  42      /**
  43       * Test summarise_response() when teachers view quiz attempts and then
  44       * review them to see what has been saved in the response history table.
  45       *
  46       * @dataProvider summarise_response_provider
  47       * @param int $responserequired
  48       * @param int $attachmentsrequired
  49       * @param string $answertext
  50       * @param int $attachmentuploaded
  51       * @param string $expected
  52       */
  53      public function test_summarise_response(int $responserequired, int $attachmentsrequired,
  54                                              string $answertext, int $attachmentuploaded, string $expected): void {
  55          $this->resetAfterTest();
  56  
  57          // If number of allowed attachments is set to 'Unlimited', generate 10 attachments for testing purpose.
  58          $numberofattachments = ($attachmentsrequired === -1) ? 10 : $attachmentsrequired;
  59  
  60          // Create sample attachments.
  61          $attachments = $this->create_user_and_sample_attachments($numberofattachments);
  62  
  63          // Create the essay question under test.
  64          $essay = \test_question_maker::make_an_essay_question();
  65          $essay->start_attempt(new question_attempt_step(), 1);
  66  
  67          $essay->responseformat = 'editor';
  68          $essay->responserequired = $responserequired;
  69          $essay->attachmentsrequired = $attachmentsrequired;
  70  
  71          // The space before the number of bytes from display_size is actually a non-breaking space.
  72          $expected = str_replace(' bytes', "\xc2\xa0bytes", $expected);
  73  
  74          $this->assertEquals($expected, $essay->summarise_response(
  75              ['answer' => $answertext, 'answerformat' => FORMAT_HTML,  'attachments' => $attachments[$attachmentuploaded]]));
  76      }
  77  
  78      /**
  79       * Data provider for summarise_response() test cases.
  80       *
  81       * @return array List of data sets (test cases)
  82       */
  83      public function summarise_response_provider(): array {
  84          return [
  85              'text input required, not attachments required'  =>
  86                  [1, 0, 'This is the text input for this essay.', 0, 'This is the text input for this essay.'],
  87              'Text input required, one attachments required, one uploaded'  =>
  88                  [1, 1, 'This is the text input for this essay.', 1, 'This is the text input for this essay.Attachments: 0 (1 bytes)'],
  89              'Text input is optional, four attachments required, one uploaded'  => [0, 4, '', 1, 'Attachments: 0 (1 bytes)'],
  90              'Text input is optional, four attachments required, two uploaded'  => [0, 4, '', 2, 'Attachments: 0 (1 bytes), 1 (1 bytes)'],
  91              'Text input is optional, four attachments required, three uploaded'  => [0, 4, '', 3, 'Attachments: 0 (1 bytes), 1 (1 bytes), 2 (1 bytes)'],
  92              'Text input is optional, four attachments required, four uploaded'  => [0, 4, 'I have attached 4 files.', 4,
  93                  'I have attached 4 files.Attachments: 0 (1 bytes), 1 (1 bytes), 2 (1 bytes), 3 (1 bytes)'],
  94              'Text input is optional, unlimited attachments required, one uploaded'  => [0, -1, '', 1, 'Attachments: 0 (1 bytes)'],
  95              'Text input is optional, unlimited attachments required, five uploaded'  => [0, -1, 'I have attached 5 files.', 5,
  96                  'I have attached 5 files.Attachments: 0 (1 bytes), 1 (1 bytes), 2 (1 bytes), 3 (1 bytes), 4 (1 bytes)'],
  97              'Text input is optional, unlimited attachments required, ten uploaded'  =>
  98                  [0, -1, '', 10, 'Attachments: 0 (1 bytes), 1 (1 bytes), 2 (1 bytes), 3 (1 bytes), 4 (1 bytes), ' .
  99                      '5 (1 bytes), 6 (1 bytes), 7 (1 bytes), 8 (1 bytes), 9 (1 bytes)']
 100          ];
 101      }
 102  
 103      public function test_is_same_response() {
 104          $essay = \test_question_maker::make_an_essay_question();
 105  
 106          $essay->responsetemplate = '';
 107  
 108          $essay->start_attempt(new question_attempt_step(), 1);
 109  
 110          $this->assertTrue($essay->is_same_response(
 111                  array(),
 112                  array('answer' => '')));
 113  
 114          $this->assertTrue($essay->is_same_response(
 115                  array('answer' => ''),
 116                  array('answer' => '')));
 117  
 118          $this->assertTrue($essay->is_same_response(
 119                  array('answer' => ''),
 120                  array()));
 121  
 122          $this->assertFalse($essay->is_same_response(
 123                  array('answer' => 'Hello'),
 124                  array()));
 125  
 126          $this->assertFalse($essay->is_same_response(
 127                  array('answer' => 'Hello'),
 128                  array('answer' => '')));
 129  
 130          $this->assertFalse($essay->is_same_response(
 131                  array('answer' => 0),
 132                  array('answer' => '')));
 133  
 134          $this->assertFalse($essay->is_same_response(
 135                  array('answer' => ''),
 136                  array('answer' => 0)));
 137  
 138          $this->assertFalse($essay->is_same_response(
 139                  array('answer' => '0'),
 140                  array('answer' => '')));
 141  
 142          $this->assertFalse($essay->is_same_response(
 143                  array('answer' => ''),
 144                  array('answer' => '0')));
 145      }
 146  
 147      public function test_is_same_response_with_template() {
 148          $essay = \test_question_maker::make_an_essay_question();
 149  
 150          $essay->responsetemplate = 'Once upon a time';
 151  
 152          $essay->start_attempt(new question_attempt_step(), 1);
 153  
 154          $this->assertTrue($essay->is_same_response(
 155                  array(),
 156                  array('answer' => 'Once upon a time')));
 157  
 158          $this->assertTrue($essay->is_same_response(
 159                  array('answer' => ''),
 160                  array('answer' => 'Once upon a time')));
 161  
 162          $this->assertTrue($essay->is_same_response(
 163                  array('answer' => 'Once upon a time'),
 164                  array('answer' => '')));
 165  
 166          $this->assertTrue($essay->is_same_response(
 167                  array('answer' => ''),
 168                  array()));
 169  
 170          $this->assertTrue($essay->is_same_response(
 171                  array('answer' => 'Once upon a time'),
 172                  array()));
 173  
 174          $this->assertFalse($essay->is_same_response(
 175                  array('answer' => 0),
 176                  array('answer' => '')));
 177  
 178          $this->assertFalse($essay->is_same_response(
 179                  array('answer' => ''),
 180                  array('answer' => 0)));
 181  
 182          $this->assertFalse($essay->is_same_response(
 183                  array('answer' => '0'),
 184                  array('answer' => '')));
 185  
 186          $this->assertFalse($essay->is_same_response(
 187                  array('answer' => ''),
 188                  array('answer' => '0')));
 189      }
 190  
 191      public function test_is_complete_response() {
 192          $this->resetAfterTest(true);
 193  
 194          // Create sample attachments.
 195          $attachments = $this->create_user_and_sample_attachments();
 196  
 197          // Create the essay question under test.
 198          $essay = \test_question_maker::make_an_essay_question();
 199          $essay->start_attempt(new question_attempt_step(), 1);
 200  
 201          // Test the "traditional" case, where we must receive a response from the user.
 202          $essay->responserequired = 1;
 203          $essay->attachmentsrequired = 0;
 204          $essay->responseformat = 'editor';
 205  
 206          // The empty string should be considered an incomplete response, as should a lack of a response.
 207          $this->assertFalse($essay->is_complete_response(array('answer' => '')));
 208          $this->assertFalse($essay->is_complete_response(array()));
 209  
 210          // Any nonempty string should be considered a complete response.
 211          $this->assertTrue($essay->is_complete_response(array('answer' => 'A student response.')));
 212          $this->assertTrue($essay->is_complete_response(array('answer' => '0 times.')));
 213          $this->assertTrue($essay->is_complete_response(array('answer' => '0')));
 214  
 215          // Test case for minimum and/or maximum word limit.
 216          $response = [];
 217          $response['answer'] = 'In this essay, I will be testing a function called check_input_word_count().';
 218  
 219          $essay->minwordlimit = 50; // The answer is shorter than the required minimum word limit.
 220          $this->assertFalse($essay->is_complete_response($response));
 221  
 222          $essay->minwordlimit = 10; // The  word count  meets the required minimum word limit.
 223          $this->assertTrue($essay->is_complete_response($response));
 224  
 225          // The word count meets the required minimum  and maximum word limit.
 226          $essay->minwordlimit = 10;
 227          $essay->maxwordlimit = 15;
 228          $this->assertTrue($essay->is_complete_response($response));
 229  
 230          // Unset the minwordlimit/maxwordlimit variables to avoid the extra check in is_complete_response() for further tests.
 231          $essay->minwordlimit = null;
 232          $essay->maxwordlimit = null;
 233  
 234          // Test the case where two files are required.
 235          $essay->attachmentsrequired = 2;
 236  
 237          // Attaching less than two files should result in an incomplete response.
 238          $this->assertFalse($essay->is_complete_response(array('answer' => 'A')));
 239          $this->assertFalse($essay->is_complete_response(
 240                  array('answer' => 'A', 'attachments' => $attachments[0])));
 241          $this->assertFalse($essay->is_complete_response(
 242                  array('answer' => 'A', 'attachments' => $attachments[1])));
 243  
 244          // Anything without response text should result in an incomplete response.
 245          $this->assertFalse($essay->is_complete_response(
 246                  array('answer' => '', 'attachments' => $attachments[2])));
 247  
 248          // Attaching two or more files should result in a complete response.
 249          $this->assertTrue($essay->is_complete_response(
 250                  array('answer' => 'A', 'attachments' => $attachments[2])));
 251          $this->assertTrue($essay->is_complete_response(
 252                  array('answer' => 'A', 'attachments' => $attachments[3])));
 253  
 254          // Test the case in which two files are required, but the inline
 255          // response is optional.
 256          $essay->responserequired = 0;
 257  
 258          $this->assertFalse($essay->is_complete_response(
 259                  array('answer' => '', 'attachments' => $attachments[1])));
 260  
 261          $this->assertTrue($essay->is_complete_response(
 262                  array('answer' => '', 'attachments' => $attachments[2])));
 263  
 264          // Test the case in which both the response and online text are optional.
 265          $essay->attachmentsrequired = 0;
 266  
 267          // Providing no answer and no attachment should result in an incomplete
 268          // response.
 269          $this->assertFalse($essay->is_complete_response(
 270                  array('answer' => '')));
 271          $this->assertFalse($essay->is_complete_response(
 272                  array('answer' => '', 'attachments' => $attachments[0])));
 273  
 274          // Providing an answer _or_ an attachment should result in a complete
 275          // response.
 276          $this->assertTrue($essay->is_complete_response(
 277                  array('answer' => '', 'attachments' => $attachments[1])));
 278          $this->assertTrue($essay->is_complete_response(
 279                  array('answer' => 'Answer text.', 'attachments' => $attachments[0])));
 280  
 281          // Test the case in which we're in "no inline response" mode,
 282          // in which the response is not required (as it's not provided).
 283          $essay->reponserequired = 0;
 284          $essay->responseformat = 'noinline';
 285          $essay->attachmensrequired = 1;
 286  
 287          $this->assertFalse($essay->is_complete_response(
 288                  array()));
 289          $this->assertFalse($essay->is_complete_response(
 290                  array('attachments' => $attachments[0])));
 291  
 292          // Providing an attachment should result in a complete response.
 293          $this->assertTrue($essay->is_complete_response(
 294                  array('attachments' => $attachments[1])));
 295  
 296          // Ensure that responserequired is ignored when we're in inline response mode.
 297          $essay->reponserequired = 1;
 298          $this->assertTrue($essay->is_complete_response(
 299                  array('attachments' => $attachments[1])));
 300      }
 301  
 302      /**
 303       * test_get_question_definition_for_external_rendering
 304       */
 305      public function test_get_question_definition_for_external_rendering() {
 306          $this->resetAfterTest();
 307  
 308          $essay = \test_question_maker::make_an_essay_question();
 309          $essay->minwordlimit = 15;
 310          $essay->start_attempt(new question_attempt_step(), 1);
 311          $qa = \test_question_maker::get_a_qa($essay);
 312          $displayoptions = new question_display_options();
 313  
 314          $options = $essay->get_question_definition_for_external_rendering($qa, $displayoptions);
 315          $this->assertNotEmpty($options);
 316          $this->assertEquals('editor', $options['responseformat']);
 317          $this->assertEquals(1, $options['responserequired']);
 318          $this->assertEquals(15, $options['responsefieldlines']);
 319          $this->assertEquals(0, $options['attachments']);
 320          $this->assertEquals(0, $options['attachmentsrequired']);
 321          $this->assertNull($options['maxbytes']);
 322          $this->assertNull($options['filetypeslist']);
 323          $this->assertEquals('', $options['responsetemplate']);
 324          $this->assertEquals(FORMAT_MOODLE, $options['responsetemplateformat']);
 325          $this->assertEquals($essay->minwordlimit, $options['minwordlimit']);
 326          $this->assertNull($options['maxwordlimit']);
 327      }
 328  
 329      /**
 330       * Test get_validation_error when users submit their input text.
 331       *
 332       * (The tests are done with a fixed 14-word response.)
 333       *
 334       * @dataProvider get_min_max_wordlimit_test_cases()
 335       * @param  int $responserequired whether response required (yes = 1, no = 0)
 336       * @param  int $minwordlimit minimum word limit
 337       * @param  int $maxwordlimit maximum word limit
 338       * @param  string $expected error message | null
 339       */
 340      public function test_get_validation_error(int $responserequired,
 341                                                int $minwordlimit, int $maxwordlimit, string $expected): void {
 342          $question = \test_question_maker::make_an_essay_question();
 343          $response = ['answer' => 'One two three four five six seven eight nine ten eleven twelve thirteen fourteen.'];
 344          $question->responserequired = $responserequired;
 345          $question->minwordlimit = $minwordlimit;
 346          $question->maxwordlimit = $maxwordlimit;
 347          $actual = $question->get_validation_error($response);
 348          $this->assertEquals($expected, $actual);
 349      }
 350  
 351      /**
 352       * Data provider for get_validation_error test.
 353       *
 354       * @return array the test cases.
 355       */
 356      public function get_min_max_wordlimit_test_cases(): array {
 357          return [
 358              'text input required, min/max word limit not set'  => [1, 0, 0, ''],
 359              'text input required, min/max word limit valid (within the boundaries)'  => [1, 10, 25, ''],
 360              'text input required, min word limit not reached'  => [1, 15, 25,
 361                  get_string('minwordlimitboundary', 'qtype_essay', ['count' => 14, 'limit' => 15])],
 362              'text input required, max word limit is exceeded'  => [1, 5, 12,
 363                  get_string('maxwordlimitboundary', 'qtype_essay', ['count' => 14, 'limit' => 12])],
 364              'text input not required, min/max word limit not set'  => [0, 5, 12, ''],
 365          ];
 366      }
 367  
 368      /**
 369       * Test get_word_count_message_for_review when users submit their input text.
 370       *
 371       * (The tests are done with a fixed 14-word response.)
 372       *
 373       * @dataProvider get_word_count_message_for_review_test_cases()
 374       * @param int|null $minwordlimit minimum word limit
 375       * @param int|null $maxwordlimit maximum word limit
 376       * @param string $expected error message | null
 377       */
 378      public function test_get_word_count_message_for_review(?int $minwordlimit, ?int $maxwordlimit, string $expected): void {
 379          $question = \test_question_maker::make_an_essay_question();
 380          $question->minwordlimit = $minwordlimit;
 381          $question->maxwordlimit = $maxwordlimit;
 382  
 383          $response = ['answer' => 'One two three four five six seven eight nine ten eleven twelve thirteen fourteen.'];
 384          $this->assertEquals($expected, $question->get_word_count_message_for_review($response));
 385      }
 386  
 387      /**
 388       * Data provider for test_get_word_count_message_for_review.
 389       *
 390       * @return array the test cases.
 391       */
 392      public function get_word_count_message_for_review_test_cases() {
 393          return [
 394              'No limit' =>
 395                      [null, null, ''],
 396              'min and max, answer within range' =>
 397                      [10, 25, get_string('wordcount', 'qtype_essay', 14)],
 398              'min and max, answer too short' =>
 399                      [15, 25, get_string('wordcounttoofew', 'qtype_essay', ['count' => 14, 'limit' => 15])],
 400              'min and max, answer too long' =>
 401                      [5, 12, get_string('wordcounttoomuch', 'qtype_essay', ['count' => 14, 'limit' => 12])],
 402              'min only, answer within range' =>
 403                      [14, null, get_string('wordcount', 'qtype_essay', 14)],
 404              'min only, answer too short' =>
 405                      [15, null, get_string('wordcounttoofew', 'qtype_essay', ['count' => 14, 'limit' => 15])],
 406              'max only, answer within range' =>
 407                      [null, 14, get_string('wordcount', 'qtype_essay', 14)],
 408              'max only, answer too short' =>
 409                      [null, 13, get_string('wordcounttoomuch', 'qtype_essay', ['count' => 14, 'limit' => 13])],
 410          ];
 411      }
 412  
 413      /**
 414       * Create sample attachemnts and retun generated attachments.
 415       * @param int $numberofattachments
 416       * @return array
 417       */
 418      private function create_user_and_sample_attachments($numberofattachments = 4) {
 419          // Create a new logged-in user, so we can test responses with attachments.
 420          $user = $this->getDataGenerator()->create_user();
 421          $this->setUser($user);
 422  
 423          // Create sample attachments to use in testing.
 424          $helper = \test_question_maker::get_test_helper('essay');
 425          $attachments = [];
 426          for ($i = 0; $i < ($numberofattachments + 1); ++$i) {
 427              $attachments[$i] = $helper->make_attachments_saver($i);
 428          }
 429          return $attachments;
 430      }
 431  }