quiz_api_test.php 8.83 KiB
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.
/**
 * The plugin uses the ASYST grading tool <https://transfer.hft-stuttgart.de/gitlab/ulrike.pado/ASYST>
 * modified to work as a web endpoint.
 * This file contains the quiz API tests for the local_asystgrade plugin.
 * @package   local_asystgrade
 * @copyright 2024 Artem Baranov
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
namespace local_asystgrade;
use advanced_testcase;
use coding_exception;
use context_course;
use dml_exception;
use Exception;
use local_asystgrade\api\client;
use local_asystgrade\api\http_client;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/phpunit/classes/advanced_testcase.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/generator/lib.php');
/**
 * Class quiz_api_test
 * @package local_asystgrade
 * @covers \local_asystgrade
final class quiz_api_test extends advanced_testcase {
    /**
     * Sets up the test environment by resetting the database and truncating quiz-related tables.
     * @throws dml_exception
    protected function setUp(): void {
        global $DB;
        $this->resetAfterTest();
        parent::setUp();
        // Clear quiz-related tables to ensure a clean test environment.
        $DB->execute('TRUNCATE TABLE {quiz_attempts}');
        $DB->execute('TRUNCATE TABLE {quiz_slots}');
    /**
     * Tests the quiz API by creating quiz attempts and validating responses.
     * @throws Exception
* @covers \local_asystgrade */ public function test_quiz_api(): void { global $DB; $generator = $this->getDataGenerator(); $quizgen = $generator->get_plugin_generator('mod_quiz'); $questiongen = $generator->get_plugin_generator('core_question'); // Create a course. $course = $generator->create_course([ 'fullname' => 'Test Course', 'shortname' => 'testcourse', 'category' => 1, ]); // Create and enroll a teacher. $teacher = $generator->create_user(); $teacherroleid = $DB->get_record('role', ['shortname' => 'teacher'])->id; $generator->enrol_user($teacher->id, $course->id, $teacherroleid); $this->setUser($teacher); // Create a quiz in the course. $quiz = $quizgen->create_instance([ 'course' => $course->id, 'name' => 'Test Quiz', 'intro' => 'This is a test quiz.', 'attempts' => 1, 'timeopen' => time(), 'timeclose' => time() + 3600, ]); // Create a question category. $context = context_course::instance($course->id); $category = $this->create_question_category($context->id); // Create sample questions and add them to the quiz. $questions = include_once('fakedata/questions.php'); foreach ($questions as $questiondata) { $question = $this->create_question($questiongen, $questiondata, $category, $teacher->id, $context); $this->add_question_to_quiz($quiz, $question); } // Create and enroll students. $students = []; for ($i = 0; $i < 7; $i++) { $students[] = $generator->create_user(); $generator->enrol_user($students[$i]->id, $course->id, 'student'); } // Create quiz attempts for students. $flatanswers = array_merge(...array_map(function($q) { return isset($q['answers']) ? array_keys($q['answers']) : []; }, $questions)); foreach ($students as $student) { $this->create_quiz_attempt($quiz->id, $student->id, $flatanswers[array_rand($flatanswers)]); } $referenceanswers = []; foreach ($questions as $questiondata) { if (isset($questiondata['answers']) && is_array($questiondata['answers'])) { foreach ($questiondata['answers'] as $answertext => $fraction) { if ($fraction == 1) { $referenceanswers[] = $answertext; } } } } $requestdata = [
'studentAnswers' => $flatanswers, 'referenceAnswer' => array_fill(0, count($flatanswers), $referenceanswers[0] ?? ''), ]; $this->send_answers_to_api($requestdata); } /** * Creates a question category. * * @param int $contextid * @return false|mixed|\stdClass * @throws coding_exception * @throws dml_exception */ private function create_question_category(int $contextid): mixed { global $DB; $category = [ 'name' => 'Test Category', 'contextid' => $contextid, 'parent' => 0, 'info' => '', 'infoformat' => FORMAT_MOODLE, ]; $categoryid = $DB->insert_record('question_categories', (object)$category); if (!$categoryid) { throw new coding_exception("Failed to create question category."); } return $DB->get_record('question_categories', ['id' => $categoryid]); } /** * Creates a question and adds it to a category. * * @param $questiongen * @param $questiondata * @param $category * @param $modifiedby * @param $context * @return mixed */ private function create_question($questiongen, $questiondata, $category, $modifiedby, $context): mixed { return $questiongen->create_question($questiondata['qtype'], null, [ 'category' => $category->id, 'questiontext' => ['text' => $questiondata['questiontext'], 'format' => FORMAT_HTML], 'name' => 'Test Question', 'contextid' => $context->id, 'modifiedby' => $modifiedby, ]); } /** * Adds a question to a quiz. * * @throws dml_exception * @param $quiz * @param $question * @return void * @throws dml_exception */ private function add_question_to_quiz($quiz, $question): void { global $DB; $slotdata = [ 'quizid' => $quiz->id, 'questionid' => $question->id, 'slot' => $DB->get_field_sql(
"SELECT COALESCE(MAX(slot), 0) + 1 FROM {quiz_slots} WHERE quizid = ?", [$quiz->id] ), 'page' => 1, 'requireprevious' => 0, 'maxmark' => 1.0, ]; $DB->insert_record('quiz_slots', (object)$slotdata); } /** * Creates a quiz attempt for a student. * @throws dml_exception * * @param $quizid * @param $userid * @param $answer * @return void * @throws dml_exception */ private function create_quiz_attempt($quizid, $userid, $answer): void { global $DB; $uniqueid = $DB->get_field_sql('SELECT COALESCE(MAX(uniqueid), 0) + 1 FROM {quiz_attempts}'); $attempt = [ 'quiz' => $quizid, 'userid' => $userid, 'attempt' => 1, 'uniqueid' => $uniqueid, 'state' => 'finished', 'timefinish' => time(), 'layout' => '', ]; $DB->insert_record('quiz_attempts', (object)$attempt); // Add student answer (mocked data). $DB->insert_record('question_attempt_step_data', [ 'attemptstepid' => $uniqueid, 'name' => 'answer', 'value' => $answer, ]); } /** * Sends answers to the API and verifies the response. * * @param $requestdata * @return void */ private function send_answers_to_api($requestdata): void { try { $apiendpoint = utils::get_api_endpoint(); $httpclient = new http_client(); $apiclient = client::getInstance($apiendpoint, $httpclient); $response = $apiclient->send_data($requestdata); $grades = json_decode($response, true); $this->assertNotEmpty($grades, 'API returned empty grades.'); } catch (Exception $e) { debugging('Error: ' . $e->getMessage()); } } }