Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 core_question;
  18  
  19  use qubaid_list;
  20  use question_bank;
  21  use question_engine;
  22  use question_filter_test_helper;
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  26  global $CFG;
  27  require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  28  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  29  
  30  /**
  31   * Tests for the {@see \core_question\local\bank\random_question_loader} class.
  32   *
  33   * @package   core_question
  34   * @copyright  2015 The Open University
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class random_question_loader_test extends \advanced_testcase {
  38  
  39      use \quiz_question_helper_test_trait;
  40  
  41      public function test_empty_category_gives_null() {
  42          $this->resetAfterTest();
  43          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  44  
  45          $cat = $generator->create_question_category();
  46          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
  47  
  48          $filters = question_filter_test_helper::create_filters([$cat->id]);
  49          $this->assertNull($loader->get_next_filtered_question_id($filters));
  50  
  51          $filters = question_filter_test_helper::create_filters([$cat->id], 1);
  52          $this->assertNull($loader->get_next_filtered_question_id($filters));
  53      }
  54  
  55      public function test_unknown_category_behaves_like_empty() {
  56          // It is up the caller to make sure the category id is valid.
  57          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
  58          $filters = question_filter_test_helper::create_filters([-1], 1);
  59          $this->assertNull($loader->get_next_filtered_question_id($filters));
  60      }
  61  
  62      public function test_descriptions_not_returned() {
  63          $this->resetAfterTest();
  64          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  65  
  66          $cat = $generator->create_question_category();
  67          $info = $generator->create_question('description', null, ['category' => $cat->id]);
  68          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
  69  
  70          $filters = question_filter_test_helper::create_filters([$cat->id]);
  71          $this->assertNull($loader->get_next_filtered_question_id($filters));
  72      }
  73  
  74      public function test_hidden_questions_not_returned() {
  75          global $DB;
  76          $this->resetAfterTest();
  77          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  78  
  79          $cat = $generator->create_question_category();
  80          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
  81          $DB->set_field('question_versions', 'status',
  82              \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $question1->id]);
  83          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
  84  
  85          $filters = question_filter_test_helper::create_filters([$cat->id]);
  86          $this->assertNull($loader->get_next_filtered_question_id($filters));
  87      }
  88  
  89      public function test_cloze_subquestions_not_returned() {
  90          $this->resetAfterTest();
  91          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
  92  
  93          $cat = $generator->create_question_category();
  94          $question1 = $generator->create_question('multianswer', null, ['category' => $cat->id]);
  95          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
  96  
  97          $filters = question_filter_test_helper::create_filters([$cat->id]);
  98          $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
  99          $this->assertNull($loader->get_next_filtered_question_id($filters));
 100      }
 101  
 102      public function test_random_questions_not_returned() {
 103          $this->resetAfterTest();
 104          $this->setAdminUser();
 105          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 106  
 107          $cat = $generator->create_question_category();
 108          $course = $this->getDataGenerator()->create_course();
 109          $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]);
 110          $this->add_random_questions($quiz->id, 1, $cat->id, 1);
 111          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 112  
 113          $filters = question_filter_test_helper::create_filters([$cat->id]);
 114          $this->assertNull($loader->get_next_filtered_question_id($filters));
 115      }
 116  
 117      public function test_one_question_category_returns_that_q_then_null() {
 118          $this->resetAfterTest();
 119          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 120  
 121          $cat = $generator->create_question_category();
 122          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 123          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 124  
 125          $filters = question_filter_test_helper::create_filters([$cat->id], 1);
 126          $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
 127  
 128          $filters = question_filter_test_helper::create_filters([$cat->id]);
 129          $this->assertNull($loader->get_next_filtered_question_id($filters));
 130      }
 131  
 132      public function test_two_question_category_returns_both_then_null() {
 133          $this->resetAfterTest();
 134          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 135  
 136          $cat = $generator->create_question_category();
 137          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 138          $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 139          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 140  
 141          $questionids = [];
 142          $filters = question_filter_test_helper::create_filters([$cat->id]);
 143          $questionids[] = $loader->get_next_filtered_question_id($filters);
 144          $questionids[] = $loader->get_next_filtered_question_id($filters);
 145          sort($questionids);
 146          $this->assertEquals([$question1->id, $question2->id], $questionids);
 147  
 148          $filters = question_filter_test_helper::create_filters([$cat->id], 1);
 149          $this->assertNull($loader->get_next_filtered_question_id($filters));
 150      }
 151  
 152      public function test_nested_categories() {
 153          $this->resetAfterTest();
 154          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 155  
 156          $cat1 = $generator->create_question_category();
 157          $cat2 = $generator->create_question_category(['parent' => $cat1->id]);
 158          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat1->id]);
 159          $question2 = $generator->create_question('shortanswer', null, ['category' => $cat2->id]);
 160          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 161  
 162          $filters = question_filter_test_helper::create_filters([$cat2->id], 1);
 163          $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
 164          $filters = question_filter_test_helper::create_filters([$cat1->id], 1);
 165          $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
 166  
 167          $filters = question_filter_test_helper::create_filters([$cat1->id]);
 168          $this->assertNull($loader->get_next_filtered_question_id($filters));
 169      }
 170  
 171      public function test_used_question_not_returned_until_later() {
 172          $this->resetAfterTest();
 173          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 174  
 175          $cat = $generator->create_question_category();
 176          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 177          $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 178          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]),
 179                  array($question2->id => 2));
 180  
 181          $filters = question_filter_test_helper::create_filters([$cat->id]);
 182          $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
 183          $this->assertNull($loader->get_next_filtered_question_id($filters));
 184      }
 185  
 186      public function test_previously_used_question_not_returned_until_later() {
 187          $this->resetAfterTest();
 188          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 189  
 190          $cat = $generator->create_question_category();
 191          $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 192          $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
 193          $quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
 194          $quba->set_preferred_behaviour('deferredfeedback');
 195          $question = question_bank::load_question($question2->id);
 196          $quba->add_question($question);
 197          $quba->add_question($question);
 198          $quba->start_all_questions();
 199          question_engine::save_questions_usage_by_activity($quba);
 200  
 201          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array($quba->get_id())));
 202  
 203          $filters = question_filter_test_helper::create_filters([$cat->id]);
 204          $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
 205          $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
 206          $this->assertNull($loader->get_next_filtered_question_id($filters));
 207      }
 208  
 209      public function test_empty_category_does_not_have_question_available() {
 210          $this->resetAfterTest();
 211          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 212  
 213          $cat = $generator->create_question_category();
 214          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
 215  
 216          $filters = question_filter_test_helper::create_filters([$cat->id]);
 217          $this->assertFalse($loader->is_filtered_question_available($filters, 1));
 218          $filters = question_filter_test_helper::create_filters([$cat->id], 1);
 219          $this->assertFalse($loader->is_filtered_question_available($filters, 1));
 220      }
 221  
 222      public function test_descriptions_not_available() {
 223          $this->resetAfterTest();
 224          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 225  
 226          $cat = $generator->create_question_category();
 227          $info = $generator->create_question('description', null, array('category' => $cat->id));
 228          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
 229  
 230          $filters = question_filter_test_helper::create_filters([$cat->id]);
 231          $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
 232          $filters = question_filter_test_helper::create_filters([$cat->id], 1);
 233          $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
 234      }
 235  
 236      public function test_existing_question_is_available_but_then_marked_used() {
 237          $this->resetAfterTest();
 238          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 239  
 240          $cat = $generator->create_question_category();
 241          $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
 242          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array()));
 243  
 244          $filters = question_filter_test_helper::create_filters([$cat->id]);
 245          $this->assertTrue($loader->is_filtered_question_available($filters, $question1->id));
 246          $this->assertFalse($loader->is_filtered_question_available($filters, $question1->id));
 247  
 248          $this->assertFalse($loader->is_filtered_question_available($filters, -1));
 249      }
 250  
 251      /**
 252       * Data provider for the get_questions test.
 253       */
 254      public function get_questions_test_cases() {
 255          return [
 256                  'empty category' => [
 257                          'categoryindex' => 'emptycat',
 258                          'includesubcategories' => false,
 259                          'usetagnames' => [],
 260                          'expectedquestionindexes' => []
 261                  ],
 262                  'single category' => [
 263                          'categoryindex' => 'cat1',
 264                          'includesubcategories' => false,
 265                          'usetagnames' => [],
 266                          'expectedquestionindexes' => ['cat1q1', 'cat1q2']
 267                  ],
 268                  'include sub category' => [
 269                          'categoryindex' => 'cat1',
 270                          'includesubcategories' => true,
 271                          'usetagnames' => [],
 272                          'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2']
 273                  ],
 274                  'single category with tags' => [
 275                          'categoryindex' => 'cat1',
 276                          'includesubcategories' => false,
 277                          'usetagnames' => ['cat1'],
 278                          'expectedquestionindexes' => ['cat1q1']
 279                  ],
 280                  'include sub category with tag on parent' => [
 281                          'categoryindex' => 'cat1',
 282                          'includesubcategories' => true,
 283                          'usetagnames' => ['cat1'],
 284                          'expectedquestionindexes' => ['cat1q1']
 285                  ],
 286                  'include sub category with tag on sub' => [
 287                          'categoryindex' => 'cat1',
 288                          'includesubcategories' => true,
 289                          'usetagnames' => ['subcat'],
 290                          'expectedquestionindexes' => ['subcatq1']
 291                  ],
 292                  'include sub category with same tag on parent and sub' => [
 293                          'categoryindex' => 'cat1',
 294                          'includesubcategories' => true,
 295                          'usetagnames' => ['foo'],
 296                          'expectedquestionindexes' => ['cat1q1', 'subcatq1']
 297                  ],
 298                  'include sub category with tag not matching' => [
 299                          'categoryindex' => 'cat1',
 300                          'includesubcategories' => true,
 301                          'usetagnames' => ['cat1', 'cat2'],
 302                          'expectedquestionindexes' => []
 303                  ]
 304          ];
 305      }
 306  
 307      /**
 308       * Test the get_questions function with various parameter combinations.
 309       *
 310       * This function creates a data set as follows:
 311       *      Category: cat1
 312       *          Question: cat1q1
 313       *              Tags: 'cat1', 'foo'
 314       *          Question: cat1q2
 315       *      Category: cat2
 316       *          Question: cat2q1
 317       *              Tags: 'cat2', 'foo'
 318       *          Question: cat2q2
 319       *      Category: subcat
 320       *          Question: subcatq1
 321       *              Tags: 'subcat', 'foo'
 322       *          Question: subcatq2
 323       *          Parent: cat1
 324       *      Category: emptycat
 325       *
 326       * @dataProvider get_questions_test_cases()
 327       * @param string $categoryindex The named index for the category to use
 328       * @param bool $includesubcategories If the search should include subcategories
 329       * @param string[] $usetagnames The tag names to include in the search
 330       * @param string[] $expectedquestionindexes The questions expected in the result
 331       */
 332      public function test_get_questions_variations(
 333              $categoryindex,
 334              $includesubcategories,
 335              $usetagnames,
 336              $expectedquestionindexes
 337      ) {
 338          $this->resetAfterTest();
 339  
 340          $categories = [];
 341          $questions = [];
 342          $tagnames = [
 343                  'cat1',
 344                  'cat2',
 345                  'subcat',
 346                  'foo'
 347          ];
 348          $collid = \core_tag_collection::get_default();
 349          $tags = \core_tag_tag::create_if_missing($collid, $tagnames);
 350          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 351  
 352          // First category and questions.
 353          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
 354          $categories['cat1'] = $category;
 355          $questions['cat1q1'] = $categoryquestions[0];
 356          $questions['cat1q2'] = $categoryquestions[1];
 357          // Second category and questions.
 358          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
 359          $categories['cat2'] = $category;
 360          $questions['cat2q1'] = $categoryquestions[0];
 361          $questions['cat2q2'] = $categoryquestions[1];
 362          // Sub category and questions.
 363          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
 364          $categories['subcat'] = $category;
 365          $questions['subcatq1'] = $categoryquestions[0];
 366          $questions['subcatq2'] = $categoryquestions[1];
 367          // Empty category.
 368          list($category, $categoryquestions) = $this->create_category_and_questions(0);
 369          $categories['emptycat'] = $category;
 370  
 371          // Generate the arguments for the get_questions function.
 372          $category = $categories[$categoryindex];
 373          $tagids = array_map(function($tagname) use ($tags) {
 374              return $tags[$tagname]->id;
 375          }, $usetagnames);
 376  
 377          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 378          $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
 379          $result = $loader->get_filtered_questions($filters);
 380          // Generate the expected question set.
 381          $expectedquestions = array_map(function($index) use ($questions) {
 382              return $questions[$index];
 383          }, $expectedquestionindexes);
 384  
 385          // Ensure the result matches what was expected.
 386          $this->assertCount(count($expectedquestions), $result);
 387          foreach ($expectedquestions as $question) {
 388              $this->assertEquals($result[$question->id]->id, $question->id);
 389              $this->assertEquals($result[$question->id]->category, $question->category);
 390          }
 391      }
 392  
 393      /**
 394       * get_questions should allow limiting and offsetting of the result set.
 395       */
 396      public function test_get_questions_with_limit_and_offset() {
 397          $this->resetAfterTest();
 398          $numberofquestions = 5;
 399          $includesubcategories = false;
 400          $tagids = [];
 401          $limit = 1;
 402          $offset = 0;
 403          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 404          list($category, $questions) = $this->create_category_and_questions($numberofquestions);
 405  
 406          // Add questionid as key to find them easily later.
 407          $questionsbyid = [];
 408          array_walk($questions, function (&$value) use (&$questionsbyid) {
 409              $questionsbyid[$value->id] = $value;
 410          });
 411          $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
 412          for ($i = 0; $i < $numberofquestions; $i++) {
 413              $result = $loader->get_filtered_questions(
 414                      $filters,
 415                      $limit,
 416                      $offset
 417              );
 418  
 419              $this->assertCount($limit, $result);
 420              $actual = array_shift($result);
 421              $expected = $questionsbyid[$actual->id];
 422              $this->assertEquals($expected->id, $actual->id);
 423              $offset++;
 424          }
 425      }
 426  
 427      /**
 428       * get_questions should allow retrieving questions with only a subset of
 429       * fields populated.
 430       */
 431      public function test_get_questions_with_restricted_fields() {
 432          $this->resetAfterTest();
 433          $includesubcategories = false;
 434          $tagids = [];
 435          $limit = 10;
 436          $offset = 0;
 437          $fields = ['id', 'name'];
 438          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 439          list($category, $questions) = $this->create_category_and_questions(1);
 440  
 441          $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
 442          $result = $loader->get_filtered_questions(
 443                  $filters,
 444                  $limit,
 445                  $offset,
 446                  $fields
 447          );
 448  
 449          $expectedquestion = array_shift($questions);
 450          $actualquestion = array_shift($result);
 451          $actualfields = get_object_vars($actualquestion);
 452          $actualfields = array_keys($actualfields);
 453          sort($actualfields);
 454          sort($fields);
 455  
 456          $this->assertEquals($fields, $actualfields);
 457      }
 458  
 459      /**
 460       * Data provider for the count_questions test.
 461       */
 462      public function count_questions_test_cases() {
 463          return [
 464                  'empty category' => [
 465                          'categoryindex' => 'emptycat',
 466                          'includesubcategories' => false,
 467                          'usetagnames' => [],
 468                          'expectedcount' => 0
 469                  ],
 470                  'single category' => [
 471                          'categoryindex' => 'cat1',
 472                          'includesubcategories' => false,
 473                          'usetagnames' => [],
 474                          'expectedcount' => 2
 475                  ],
 476                  'include sub category' => [
 477                          'categoryindex' => 'cat1',
 478                          'includesubcategories' => true,
 479                          'usetagnames' => [],
 480                          'expectedcount' => 4
 481                  ],
 482                  'single category with tags' => [
 483                          'categoryindex' => 'cat1',
 484                          'includesubcategories' => false,
 485                          'usetagnames' => ['cat1'],
 486                          'expectedcount' => 1
 487                  ],
 488                  'include sub category with tag on parent' => [
 489                          'categoryindex' => 'cat1',
 490                          'includesubcategories' => true,
 491                          'usetagnames' => ['cat1'],
 492                          'expectedcount' => 1
 493                  ],
 494                  'include sub category with tag on sub' => [
 495                          'categoryindex' => 'cat1',
 496                          'includesubcategories' => true,
 497                          'usetagnames' => ['subcat'],
 498                          'expectedcount' => 1
 499                  ],
 500                  'include sub category with same tag on parent and sub' => [
 501                          'categoryindex' => 'cat1',
 502                          'includesubcategories' => true,
 503                          'usetagnames' => ['foo'],
 504                          'expectedcount' => 2
 505                  ],
 506                  'include sub category with tag not matching' => [
 507                          'categoryindex' => 'cat1',
 508                          'includesubcategories' => true,
 509                          'usetagnames' => ['cat1', 'cat2'],
 510                          'expectedcount' => 0
 511                  ]
 512          ];
 513      }
 514  
 515      /**
 516       * Test the count_questions function with various parameter combinations.
 517       *
 518       * This function creates a data set as follows:
 519       *      Category: cat1
 520       *          Question: cat1q1
 521       *              Tags: 'cat1', 'foo'
 522       *          Question: cat1q2
 523       *      Category: cat2
 524       *          Question: cat2q1
 525       *              Tags: 'cat2', 'foo'
 526       *          Question: cat2q2
 527       *      Category: subcat
 528       *          Question: subcatq1
 529       *              Tags: 'subcat', 'foo'
 530       *          Question: subcatq2
 531       *          Parent: cat1
 532       *      Category: emptycat
 533       *
 534       * @dataProvider count_questions_test_cases()
 535       * @param string $categoryindex The named index for the category to use
 536       * @param bool $includesubcategories If the search should include subcategories
 537       * @param string[] $usetagnames The tag names to include in the search
 538       * @param int $expectedcount The number of questions expected in the result
 539       */
 540      public function test_count_questions_variations(
 541              $categoryindex,
 542              $includesubcategories,
 543              $usetagnames,
 544              $expectedcount
 545      ) {
 546          $this->resetAfterTest();
 547  
 548          $categories = [];
 549          $questions = [];
 550          $tagnames = [
 551                  'cat1',
 552                  'cat2',
 553                  'subcat',
 554                  'foo'
 555          ];
 556          $collid = \core_tag_collection::get_default();
 557          $tags = \core_tag_tag::create_if_missing($collid, $tagnames);
 558          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 559  
 560          // First category and questions.
 561          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
 562          $categories['cat1'] = $category;
 563          $questions['cat1q1'] = $categoryquestions[0];
 564          $questions['cat1q2'] = $categoryquestions[1];
 565          // Second category and questions.
 566          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
 567          $categories['cat2'] = $category;
 568          $questions['cat2q1'] = $categoryquestions[0];
 569          $questions['cat2q2'] = $categoryquestions[1];
 570          // Sub category and questions.
 571          list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
 572          $categories['subcat'] = $category;
 573          $questions['subcatq1'] = $categoryquestions[0];
 574          $questions['subcatq2'] = $categoryquestions[1];
 575          // Empty category.
 576          list($category, $categoryquestions) = $this->create_category_and_questions(0);
 577          $categories['emptycat'] = $category;
 578  
 579          // Generate the arguments for the get_questions function.
 580          $category = $categories[$categoryindex];
 581          $tagids = array_map(function($tagname) use ($tags) {
 582              return $tags[$tagname]->id;
 583          }, $usetagnames);
 584  
 585          $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
 586          $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
 587          $result = $loader->count_filtered_questions($filters);
 588  
 589          // Ensure the result matches what was expected.
 590          $this->assertEquals($expectedcount, $result);
 591      }
 592  
 593      /**
 594       * Create a question category and create questions in that category. Tag
 595       * the first question in each category with the given tags.
 596       *
 597       * @param int $questioncount How many questions to create.
 598       * @param string[] $tagnames The list of tags to use.
 599       * @param stdClass|null $parentcategory The category to set as the parent of the created category.
 600       * @return array The category and questions.
 601       */
 602      protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null) {
 603          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 604  
 605          if ($parentcategory) {
 606              $catparams = ['parent' => $parentcategory->id];
 607          } else {
 608              $catparams = [];
 609          }
 610  
 611          $category = $generator->create_question_category($catparams);
 612          $questions = [];
 613  
 614          for ($i = 0; $i < $questioncount; $i++) {
 615              $questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]);
 616          }
 617  
 618          if (!empty($tagnames) && !empty($questions)) {
 619              $context = \context::instance_by_id($category->contextid);
 620              \core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames);
 621          }
 622  
 623          return [$category, $questions];
 624      }
 625  }