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 grading evaluation method "best"
19 *
20 * @package    workshopeval_best
21 * @category   phpunit
22 * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28// Include the code to test
29global $CFG;
30require_once($CFG->dirroot . '/mod/workshop/locallib.php');
31require_once($CFG->dirroot . '/mod/workshop/eval/best/lib.php');
32require_once($CFG->libdir . '/gradelib.php');
33
34
35class workshopeval_best_evaluation_testcase extends advanced_testcase {
36
37    /** workshop instance emulation */
38    protected $workshop;
39
40    /** instance of the grading evaluator being tested */
41    protected $evaluator;
42
43    /**
44     * Setup testing environment
45     */
46    protected function setUp(): void {
47        parent::setUp();
48        $this->resetAfterTest();
49        $this->setAdminUser();
50        $course = $this->getDataGenerator()->create_course();
51        $workshop = $this->getDataGenerator()->create_module('workshop', array('evaluation' => 'best', 'course' => $course));
52        $cm = get_fast_modinfo($course)->instances['workshop'][$workshop->id];
53        $this->workshop = new workshop($workshop, $cm, $course);
54        $this->evaluator = new testable_workshop_best_evaluation($this->workshop);
55    }
56
57    protected function tearDown(): void {
58        $this->workshop = null;
59        $this->evaluator = null;
60        parent::tearDown();
61    }
62
63    public function test_normalize_grades() {
64        // fixture set-up
65        $assessments = array();
66        $assessments[1] = (object)array(
67            'dimgrades' => array(3 => 1.0000, 4 => 13.42300),
68        );
69        $assessments[3] = (object)array(
70            'dimgrades' => array(3 => 2.0000, 4 => 19.1000),
71        );
72        $assessments[7] = (object)array(
73            'dimgrades' => array(3 => 3.0000, 4 => 0.00000),
74        );
75        $diminfo = array(
76            3 => (object)array('min' => 1, 'max' => 3),
77            4 => (object)array('min' => 0, 'max' => 20),
78        );
79        // exercise SUT
80        $norm = $this->evaluator->normalize_grades($assessments, $diminfo);
81        // validate
82        $this->assertEquals(gettype($norm), 'array');
83        // the following grades from a scale
84        $this->assertEquals($norm[1]->dimgrades[3], 0);
85        $this->assertEquals($norm[3]->dimgrades[3], 50);
86        $this->assertEquals($norm[7]->dimgrades[3], 100);
87        // the following grades from an interval 0 - 20
88        $this->assertEquals($norm[1]->dimgrades[4], grade_floatval(13.423 / 20 * 100));
89        $this->assertEquals($norm[3]->dimgrades[4], grade_floatval(19.1 / 20 * 100));
90        $this->assertEquals($norm[7]->dimgrades[4], 0);
91    }
92
93    public function test_normalize_grades_max_equals_min() {
94        // fixture set-up
95        $assessments = array();
96        $assessments[1] = (object)array(
97            'dimgrades' => array(3 => 100.0000),
98        );
99        $diminfo = array(
100            3 => (object)array('min' => 100, 'max' => 100),
101        );
102        // exercise SUT
103        $norm = $this->evaluator->normalize_grades($assessments, $diminfo);
104        // validate
105        $this->assertEquals(gettype($norm), 'array');
106        $this->assertEquals($norm[1]->dimgrades[3], 100);
107    }
108
109    public function test_average_assessment_same_weights() {
110        // fixture set-up
111        $assessments = array();
112        $assessments[18] = (object)array(
113            'weight'        => 1,
114            'dimgrades'     => array(1 => 50, 2 => 33.33333),
115        );
116        $assessments[16] = (object)array(
117            'weight'        => 1,
118            'dimgrades'     => array(1 => 0, 2 => 66.66667),
119        );
120        // exercise SUT
121        $average = $this->evaluator->average_assessment($assessments);
122        // validate
123        $this->assertEquals(gettype($average->dimgrades), 'array');
124        $this->assertEquals(grade_floatval($average->dimgrades[1]), grade_floatval(25));
125        $this->assertEquals(grade_floatval($average->dimgrades[2]), grade_floatval(50));
126    }
127
128    public function test_average_assessment_different_weights() {
129        // fixture set-up
130        $assessments = array();
131        $assessments[11] = (object)array(
132            'weight'        => 1,
133            'dimgrades'     => array(3 => 10.0, 4 => 13.4, 5 => 95.0),
134        );
135        $assessments[13] = (object)array(
136            'weight'        => 3,
137            'dimgrades'     => array(3 => 11.0, 4 => 10.1, 5 => 92.0),
138        );
139        $assessments[17] = (object)array(
140            'weight'        => 1,
141            'dimgrades'     => array(3 => 11.0, 4 => 8.1, 5 => 88.0),
142        );
143        // exercise SUT
144        $average = $this->evaluator->average_assessment($assessments);
145        // validate
146        $this->assertEquals(gettype($average->dimgrades), 'array');
147        $this->assertEquals(grade_floatval($average->dimgrades[3]), grade_floatval((10.0 + 11.0*3 + 11.0)/5));
148        $this->assertEquals(grade_floatval($average->dimgrades[4]), grade_floatval((13.4 + 10.1*3 + 8.1)/5));
149        $this->assertEquals(grade_floatval($average->dimgrades[5]), grade_floatval((95.0 + 92.0*3 + 88.0)/5));
150    }
151
152    public function test_average_assessment_noweight() {
153        // fixture set-up
154        $assessments = array();
155        $assessments[11] = (object)array(
156            'weight'        => 0,
157            'dimgrades'     => array(3 => 10.0, 4 => 13.4, 5 => 95.0),
158        );
159        $assessments[17] = (object)array(
160            'weight'        => 0,
161            'dimgrades'     => array(3 => 11.0, 4 => 8.1, 5 => 88.0),
162        );
163        // exercise SUT
164        $average = $this->evaluator->average_assessment($assessments);
165        // validate
166        $this->assertNull($average);
167    }
168
169    public function test_weighted_variance() {
170        // fixture set-up
171        $assessments[11] = (object)array(
172            'weight'        => 1,
173            'dimgrades'     => array(3 => 11, 4 => 2),
174        );
175        $assessments[13] = (object)array(
176            'weight'        => 3,
177            'dimgrades'     => array(3 => 11, 4 => 4),
178        );
179        $assessments[17] = (object)array(
180            'weight'        => 2,
181            'dimgrades'     => array(3 => 11, 4 => 5),
182        );
183        $assessments[20] = (object)array(
184            'weight'        => 1,
185            'dimgrades'     => array(3 => 11, 4 => 7),
186        );
187        $assessments[25] = (object)array(
188            'weight'        => 1,
189            'dimgrades'     => array(3 => 11, 4 => 9),
190        );
191        // exercise SUT
192        $variance = $this->evaluator->weighted_variance($assessments);
193        // validate
194        // dimension [3] have all the grades equal to 11
195        $this->assertEquals($variance[3], 0);
196        // dimension [4] represents data 2, 4, 4, 4, 5, 5, 7, 9 having stdev=2 (stdev is sqrt of variance)
197        $this->assertEquals($variance[4], 4);
198    }
199
200    public function test_assessments_distance_zero() {
201        // fixture set-up
202        $diminfo = array(
203            3 => (object)array('weight' => 1, 'min' => 0, 'max' => 100, 'variance' => 12.34567),
204            4 => (object)array('weight' => 1, 'min' => 1, 'max' => 5,   'variance' => 98.76543),
205        );
206        $assessment1 = (object)array('dimgrades' => array(3 => 15, 4 => 2));
207        $assessment2 = (object)array('dimgrades' => array(3 => 15, 4 => 2));
208        $settings = (object)array('comparison' => 5);
209        // exercise SUT and validate
210        $this->assertEquals($this->evaluator->assessments_distance($assessment1, $assessment2, $diminfo, $settings), 0);
211    }
212
213    public function test_assessments_distance_equals() {
214        /*
215        // fixture set-up
216        $diminfo = array(
217            3 => (object)array('weight' => 1, 'min' => 0, 'max' => 100, 'variance' => 12.34567),
218            4 => (object)array('weight' => 1, 'min' => 0, 'max' => 100, 'variance' => 12.34567),
219        );
220        $assessment1 = (object)array('dimgrades' => array(3 => 25, 4 => 4));
221        $assessment2 = (object)array('dimgrades' => array(3 => 75, 4 => 2));
222        $referential = (object)array('dimgrades' => array(3 => 50, 4 => 3));
223        $settings = (object)array('comparison' => 5);
224        // exercise SUT and validate
225        $this->assertEquals($this->evaluator->assessments_distance($assessment1, $referential, $diminfo, $settings),
226                           $this->evaluator->assessments_distance($assessment2, $referential, $diminfo, $settings));
227        */
228        // fixture set-up
229        $diminfo = array(
230            1 => (object)array('min' => 0, 'max' => 2, 'weight' => 1, 'variance' => 625),
231            2 => (object)array('min' => 0, 'max' => 3, 'weight' => 1, 'variance' => 277.7778888889),
232        );
233        $assessment1 = (object)array('dimgrades' => array(1 => 0,  2 => 66.66667));
234        $assessment2 = (object)array('dimgrades' => array(1 => 50, 2 => 33.33333));
235        $referential = (object)array('dimgrades' => array(1 => 25, 2 => 50));
236        $settings = (object)array('comparison' => 9);
237        // exercise SUT and validate
238        $this->assertEquals($this->evaluator->assessments_distance($assessment1, $referential, $diminfo, $settings),
239            $this->evaluator->assessments_distance($assessment2, $referential, $diminfo, $settings));
240
241    }
242
243    public function test_assessments_distance_zero_variance() {
244        // Fixture set-up: an assessment form of the strategy "Number of errors",
245        // three assertions, same weight.
246        $diminfo = array(
247            1 => (object)array('min' => 0, 'max' => 1, 'weight' => 1),
248            2 => (object)array('min' => 0, 'max' => 1, 'weight' => 1),
249            3 => (object)array('min' => 0, 'max' => 1, 'weight' => 1),
250        );
251
252        // Simulate structure returned by {@link workshop_best_evaluation::prepare_data_from_recordset()}
253        $assessments = array(
254            // The first assessment has weight 0 and the assessment was No, No, No.
255            10 => (object)array(
256                'assessmentid' => 10,
257                'weight' => 0,
258                'reviewerid' => 56,
259                'gradinggrade' => null,
260                'submissionid' => 99,
261                'dimgrades' => array(
262                    1 => 0,
263                    2 => 0,
264                    3 => 0,
265                ),
266            ),
267            // The second assessment has weight 1 and assessments was Yes, Yes, Yes.
268            20 => (object)array(
269                'assessmentid' => 20,
270                'weight' => 1,
271                'reviewerid' => 76,
272                'gradinggrade' => null,
273                'submissionid' => 99,
274                'dimgrades' => array(
275                    1 => 1,
276                    2 => 1,
277                    3 => 1,
278                ),
279            ),
280            // The third assessment has weight 1 and assessments was Yes, Yes, Yes too.
281            30 => (object)array(
282                'assessmentid' => 30,
283                'weight' => 1,
284                'reviewerid' => 97,
285                'gradinggrade' => null,
286                'submissionid' => 99,
287                'dimgrades' => array(
288                    1 => 1,
289                    2 => 1,
290                    3 => 1,
291                ),
292            ),
293        );
294
295        // Process assessments in the same way as in the {@link workshop_best_evaluation::process_assessments()}
296        $assessments = $this->evaluator->normalize_grades($assessments, $diminfo);
297        $average = $this->evaluator->average_assessment($assessments);
298        $variances = $this->evaluator->weighted_variance($assessments);
299        foreach ($variances as $dimid => $variance) {
300            $diminfo[$dimid]->variance = $variance;
301        }
302
303        // Simulate the chosen comparison of assessments "fair" (does not really matter here but we need something).
304        $settings = (object)array('comparison' => 5);
305
306        // Exercise SUT: for every assessment, calculate its distance from the average one.
307        $distances = array();
308        foreach ($assessments as $asid => $assessment) {
309            $distances[$asid] = $this->evaluator->assessments_distance($assessment, $average, $diminfo, $settings);
310        }
311
312        // Validate: the first assessment is far far away from the average one ...
313        $this->assertTrue($distances[10] > 0);
314        // ... while the two others were both picked as the referential ones.
315        $this->assertTrue($distances[20] == 0);
316        $this->assertTrue($distances[30] == 0);
317    }
318}
319
320
321/**
322 * Test subclass that makes all the protected methods we want to test public.
323 */
324class testable_workshop_best_evaluation extends workshop_best_evaluation {
325
326    public function normalize_grades(array $assessments, array $diminfo) {
327        return parent::normalize_grades($assessments, $diminfo);
328    }
329    public function average_assessment(array $assessments) {
330        return parent::average_assessment($assessments);
331    }
332    public function weighted_variance(array $assessments) {
333        return parent::weighted_variance($assessments);
334    }
335    public function assessments_distance(stdclass $assessment, stdclass $referential, array $diminfo, stdclass $settings) {
336        return parent::assessments_distance($assessment, $referential, $diminfo, $settings);
337    }
338}
339