ILIAS  trunk Revision v12.0_alpha-1540-g00f839d5fa1
TestResultRepositoryTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
24use ILIAS\Refinery\Factory as Refinery;
33use PHPUnit\Framework\MockObject\MockObject;
34use PHPUnit\Framework\Attributes\DataProvider;
35
37{
38 public function testConstruct(): void
39 {
40 $repository = $this->createInstance();
41 $this->assertInstanceOf(Repository::class, $repository);
42 }
43
44 #[DataProvider('providePassedParticipants')]
45 public function testGetPassedParticipants(int $test_obj_id, array $query_result): void
46 {
47 $this->mockGetPassedParticipants($query_result);
48 $repository = $this->createInstance();
49
50 $actual = $repository->getPassedParticipants($test_obj_id);
51 foreach ($actual as $index => $participant) {
52 $this->assertEquals($participant['active_id'], $query_result[$index]['active_id']);
53 $this->assertEquals($participant['user_id'], $query_result[$index]['user_id']);
54 }
55 }
56
57 #[DataProvider('provideTestResultCache')]
58 public function testGetTestResult(array $query_result, array $expected): void
59 {
60 $this->mockGetTestResultQuery($query_result);
61 $repository = $this->createInstance($query_result);
62
63 $actual = $repository->getTestResult($query_result['active_fi']);
64
65 $this->assertNotNull($actual);
66 $this->assertInstanceOf(ParticipantResult::class, $actual);
67 foreach ($expected as $method => $value) {
68 $this->assertEquals($value, $actual->$method());
69 }
70 }
71
72 public function testGetTestResultNotFound(): void
73 {
74 $this->mockGetTestResultQuery(null);
75 $repository = $this->createInstance();
76
77 $actual = $repository->getTestResult(1000);
78
79 $this->assertNull($actual);
80 }
81
82 #[DataProvider('provideFetchedTestAttemptResult')]
83 public function testReadStatus(array $query_result, array $expected): void
84 {
85 $this->mockUpdateTestResultCache($query_result);
86 $repository = $this->createInstance();
87 $repository->updateTestResultCache($query_result['active_fi']);
88
89 $user_id = $query_result['user_id'];
90 $test_obj_id = $query_result['test_obj_id'];
91
92 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
93 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
94 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
95 }
96
97 public function testFailedPassedNotFound(): void
98 {
99 $repository = $this->createInstance();
100
101 $this->assertFalse($repository->isPassed(100, 200));
102 $this->assertFalse($repository->isFailed(100, 200));
103 $this->assertFalse($repository->hasFinished(100, 200));
104 }
105
106 #[DataProvider('provideCachedStatus')]
107 public function testReadFromCache(array $query, array $cached_status, array $expected): void
108 {
109 $repository = $this->createInstance();
110 $user_id = $query['user_id'];
111 $test_obj_id = $query['test_obj_id'];
112
113 // Ensure the data is queried from the database, as it is not yet in the cache
114 $this->mockReadResultStatusQuery($cached_status);
115
116 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
117 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
118 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
119
120 // Ensure the database is not queried again
121 $this->adaptDICServiceMock(
122 \ilDBInterface::class,
123 function (\ilDBInterface|MockObject $mock) {
124 $mock->expects($this->exactly(0))->method('queryF');
125 $mock->expects($this->exactly(0))->method('fetchAssoc');
126 }
127 );
128
129 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
130 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
131 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
132 }
133
134 #[DataProvider('provideCachedStatus')]
135 public function testRemoveTestResults(array $query, array $cached_status, array $expected): void
136 {
137 $repository = $this->createInstance();
138 $user_id = $query['user_id'];
139 $active_id = $query['active_id'];
140 $test_obj_id = $query['test_obj_id'];
141
142 // Ensure the data is loaded from the database, as it is not yet in the cache
143 $this->mockReadResultStatusQuery($cached_status);
144
145 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
146 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
147 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
148
149 $this->mockGetUserIds([['user_fi' => $user_id]]);
150 $repository->removeTestResults([$active_id], $test_obj_id);
151
152 // Ensure the data is queried again
153 $this->mockReadResultStatusQuery($cached_status);
154
155 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
156 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
157 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
158 }
159
160 #[DataProvider('provideTestAttemptResult')]
161 public function testGetTestAttemptResult(array $query_result, array $expected): void
162 {
163 $this->mockGetTestPassResultQuery($query_result);
164 $repository = $this->createInstance();
165
166 $actual = $repository->getTestAttemptResult($query_result['active_fi']);
167
168 $this->assertNotNull($actual);
169 $this->assertInstanceOf(AttemptResult::class, $actual);
170 foreach ($expected as $method => $value) {
171 $this->assertEquals($value, $actual->$method());
172 }
173 }
174
175 public function testGetTestAttemptResultNotFound(): void
176 {
177 $this->mockGetTestPassResultQuery(null);
178 $repository = $this->createInstance();
179
180 $actual = $repository->getTestAttemptResult(1000);
181
182 $this->assertNull($actual);
183 }
184
185 #[DataProvider('provideFetchedTestResult')]
187 array $parameters,
188 array $test_result,
189 array $test_config_result,
190 array $working_time_result,
191 array $expected
192 ): void {
193 $this->mockUpdateTestAttemptResult($test_result, $test_config_result, $working_time_result);
194 $repository = $this->createInstance();
195
196 $actual = $repository->updateTestAttemptResult(
197 $parameters['active_id'],
198 $parameters['pass'],
199 null,
200 $parameters['test_obj_id'],
201 false
202 );
203
204 $this->assertNotNull($actual);
205 $this->assertInstanceOf(AttemptResult::class, $actual);
206 $this->assertEqualsWithDelta(time(), $actual->getTimestamp(), 5);
207 foreach ($expected as $method => $value) {
208 $this->assertEquals($value, $actual->$method());
209 }
210 }
211
212 /*
213 Mocking
214 */
215
216 private function createInstance(?array $mock_data = null): Repository
217 {
218 global $DIC;
219
220 $global_cache = $this->createConfiguredMock(
221 \ILIAS\Cache\Services::class,
222 ['get' => $this->createCacheMock()]
223 );
224
225 $partial_mock = $this->getMockBuilder(Repository::class)
226 ->disableOriginalClone()
227 ->setConstructorArgs([
228 $DIC->database(),
229 $this->createMock(Refinery::class),
230 $this->createMarksRepositoryMock($mock_data),
231 $global_cache
232 ])
233 ->onlyMethods(['lookupAttempt'])
234 ->getMock();
235 $partial_mock->method('lookupAttempt')->willReturn(0);
236
237 return $partial_mock;
238 }
239
240 private function createMarksRepositoryMock(?array $mock_data): MarksRepository
241 {
242 if ($mock_data) {
243 $mock = new Mark(
244 $mock_data['mark_short'],
245 $mock_data['mark_official'],
246 0.0,
247 (bool) $mock_data['passed']
248 );
249 $mark_schema = $this->createConfiguredMock(
250 MarkSchema::class,
251 ['getMatchingMark' => $mock]
252 );
253 } else {
254 $mark_schema = (new MarkSchemaFactory())->createSimpleSchema(0);
255 }
256
257 return new class ($mark_schema) implements MarksRepository {
258 public function __construct(protected MarkSchema $mark_schema)
259 {
260 }
261
262 public function getMarkSchemaFor(int $test_id): MarkSchema
263 {
264 return $this->mark_schema;
265 }
266
267 public function storeMarkSchema(MarkSchema $mark_schema): array
268 {
269 throw new \Error('Not implemented');
270 }
271
272 public function getMarkSchemaBySteps(array $step_ids): MarkSchema
273 {
274 throw new \Error('Not implemented');
275 }
276
277 public function deleteSteps(array $step_ids): void
278 {
279 throw new \Error('Not implemented');
280 }
281 };
282 }
283
284 private function createCacheMock(): Container
285 {
286 return new class () implements Container {
287 private array $cache = [];
288
289 public function lock(float $seconds): void
290 {
291 throw new \Error('Not implemented');
292 }
293
294 public function isLocked(): bool
295 {
296 throw new \Error('Not implemented');
297 }
298
299 public function has(string $key): bool
300 {
301 return isset($this->cache[$key]);
302 }
303
304 public function get(string $key, Transformation $transformation): string|int|array|bool|null
305 {
306 return $this->cache[$key] ?? null;
307 }
308
309 public function set(string $key, string|int|array|bool|null $value, ?int $ttl = null): void
310 {
311 $this->cache[$key] = $value;
312 }
313
314 public function delete(string $key): void
315 {
316 unset($this->cache[$key]);
317 }
318
319 public function flush(): void
320 {
321 $this->cache = [];
322 }
323
324 public function getAdaptorName(): string
325 {
326 throw new \Error('Not implemented');
327 }
328
329 public function getContainerName(): string
330 {
331 throw new \Error('Not implemented');
332 }
333 };
334 }
335
336 /*
337 Database Mocking
338 */
339
343 private function mockGetPassedParticipants(array $fetch_all_return): void
344 {
345 $this->adaptDICServiceMock(
346 \ilDBInterface::class,
347 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
348 $mock
349 ->expects($this->once())
350 ->method('queryF')
351 ->with($this->stringContains("WHERE tst_tests.obj_fi = %s AND tst_result_cache.passed_once = 1"));
352
353 $mock
354 ->expects($this->once())
355 ->method('fetchAll')
356 ->willReturn($fetch_all_return);
357 }
358 );
359 }
360
364 private function mockGetTestResultQuery(?array $fetch_assoc_return): void
365 {
366 if ($fetch_assoc_return) {
367 $fetch_assoc_return['test_id'] = 0;
368 }
369 $this->mockGetResultQuery('tst_result_cache', $fetch_assoc_return);
370 }
371
375 private function mockGetTestPassResultQuery(?array $fetch_assoc_return): void
376 {
377 $this->mockGetResultQuery('tst_pass_result', $fetch_assoc_return);
378 }
379
380 private function mockGetResultQuery(string $table, ?array $fetch_assoc_return): void
381 {
382 $this->adaptDICServiceMock(
383 \ilDBInterface::class,
384 function (\ilDBInterface|MockObject $mock) use ($table, $fetch_assoc_return) {
385 $mock
386 ->expects($this->once())
387 ->method('queryF');
388
389 $mock
390 ->expects($this->once())
391 ->method('fetchAssoc')
392 ->willReturn($fetch_assoc_return);
393 }
394 );
395 }
396
400 private function mockReadResultStatusQuery(?array $fetch_assoc_return): void
401 {
402 $this->adaptDICServiceMock(
403 \ilDBInterface::class,
404 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_return) {
405 $mock->expects($this->atLeastOnce())->method('queryF');
406 $mock->expects($this->atLeastOnce())->method('fetchAssoc')->willReturn($fetch_assoc_return);
407 }
408 );
409 }
410
414 private function mockGetUserIds(?array $fetch_all_return): void
415 {
416 $this->adaptDICServiceMock(
417 \ilDBInterface::class,
418 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
419 $mock->expects($this->once())
420 ->method('query')
421 ->with($this->equalTo("SELECT user_fi FROM tst_active WHERE\n"));
422 $mock->expects($this->once())->method('fetchAll')->willReturn($fetch_all_return);
423 }
424 );
425 }
426
430 private function mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once = false): void
431 {
432 $this->adaptDICServiceMock(
433 \ilDBInterface::class,
434 function (\ilDBInterface|MockObject $mock) use ($test_attempt_result) {
435 // Ensures that the check whether results are available is mocked
436 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
437 'numRows' => 1,
438 ]);
439 $mock->method('queryF')->willReturn($mocked_stmt);
440
441 // TestResultRepository::fetchTestPassResult
442 $mock->expects($this->exactly(1))
443 ->method('fetchAssoc')
444 ->willReturn($test_attempt_result);
445
446 $mock->expects($this->exactly(1))->method('replace');
447 }
448 );
449 }
450
454 private function mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time): void
455 {
456 $fetch_assoc_mocks = [
457 $test_result, // TestResultRepository::fetchTestResult
458 ['question_set_type' => \ilObjTest::QUESTION_SET_TYPE_FIXED], // TestResultRepository::fetchAdditionalTestData (1)
459 $test_config, // TestResultRepository::fetchAdditionalTestData (2)
460 $working_time, // TestResultRepository::fetchWorkingTime
461 null // TestResultRepository::fetchWorkingTime (i2)
462 ];
463
464 $this->adaptDICServiceMock(
465 \ilDBInterface::class,
466 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_mocks) {
467 // Ensures that the check whether results are available is mocked
468 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
469 'numRows' => 1,
470 ]);
471 $mock->method('queryF')->willReturn($mocked_stmt);
472
473 $mock->expects($this->exactly(count($fetch_assoc_mocks)))
474 ->method('fetchAssoc')
475 ->willReturnOnConsecutiveCalls(...$fetch_assoc_mocks);
476
477 $mock->expects($this->exactly(1))->method('replace');
478 }
479 );
480 }
481
482 /*
483 Data Provider
484 */
485
489 public static function provideCachedStatus(): array
490 {
491 return [
492 [
493 ['user_id' => 1, 'test_obj_id' => 100, 'active_id' => 1000],
494 ['passed' => true, 'failed' => false, 'finished' => false],
495 ['isPassed' => true, 'isFailed' => false, 'hasFinished' => false],
496 ],
497 [
498 ['user_id' => 10, 'test_obj_id' => 100, 'active_id' => 1000],
499 ['passed' => false, 'failed' => true, 'finished' => false],
500 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => false],
501 ],
502 [
503 ['user_id' => 1, 'test_obj_id' => 250, 'active_id' => 1400],
504 ['passed' => false, 'failed' => true, 'finished' => true],
505 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => true],
506 ]
507 ];
508 }
509
519 public static function providePassedParticipants(): array
520 {
521 return [
522 [
523 10,
524 [
525 ['user_id' => 1, 'active_id' => 100],
526 ['user_id' => 2, 'active_id' => 200],
527 ['user_id' => 3, 'active_id' => 101],
528 ['user_id' => 4, 'active_id' => 201],
529 ['user_id' => 5, 'active_id' => 0],
530 ]
531 ],
532 ];
533 }
534
542 public static function provideTestResultCache(): array
543 {
544 return [
545 // Dataset #1: failed result
546 [
547 [
548 'active_fi' => 10,
549 'pass' => 0,
550 'max_points' => 25,
551 'reached_points' => 0,
552 'mark_short' => 'failed',
553 'mark_official' => 'failed',
554 'passed' => 0,
555 'failed' => 1,
556 'tstamp' => 1740557748,
557 'passed_once' => 0
558 ],
559 [
560 'getActiveId' => 10,
561 'isPassed' => false,
562 'isPassedOnce' => false,
563 'isFailed' => true,
564 'getAttempt' => 0,
565 'getMaxPoints' => 25,
566 'getReachedPoints' => 0,
567 'getMarkShort' => 'failed',
568 'getMarkOfficial' => 'failed',
569 'getTimestamp' => 1740557748,
570 ]
571 ],
572 ];
573 }
574
585 public static function provideFetchedTestAttemptResult(): array
586 {
587 return [
588 // Dataset #1: failed result
589 [
590 [
591 'active_fi' => 10,
592 'pass' => 0,
593 'maxpoints' => 0,
594 'questioncount' => 2,
595 'answeredquestions' => 2,
596 'workingtime' => 12,
597 'tstamp' => 1740557748,
598 'exam_id' => 'I0_T334_A41_P0',
599 'finalized_by' => null,
600 'last_finished_pass' => 0,
601 'user_id' => 1,
602 'test_id' => 5,
603 'test_obj_id' => 100,
604 'max_points' => 25,
605 'reached_points' => 0,
606 ],
607 [
608 'getActiveId' => 10,
609 'isPassed' => false,
610 'isPassedOnce' => false,
611 'isFailed' => true,
612 'hasFinished' => true,
613 'getAttempt' => 0,
614 'getMaxPoints' => 25,
615 'getReachedPoints' => 0,
616 'getMarkShort' => 'failed',
617 'getMarkOfficial' => 'failed',
618 'getTimestamp' => 1740557748,
619 ]
620 ],
621 // Dataset #2: success result
622 [
623 [
624 'active_fi' => 11,
625 'pass' => 0,
626 'maxpoints' => 0,
627 'questioncount' => 2,
628 'answeredquestions' => 2,
629 'workingtime' => 12,
630 'tstamp' => 1740557748,
631 'exam_id' => 'I0_T334_A41_P0',
632 'finalized_by' => null,
633 'last_finished_pass' => 1,
634 'user_id' => 1,
635 'test_id' => 5,
636 'test_obj_id' => 100,
637 'max_points' => 25,
638 'reached_points' => 25,
639 ],
640 [
641 'getActiveId' => 11,
642 'isPassed' => true,
643 'isPassedOnce' => true,
644 'isFailed' => false,
645 'hasFinished' => true,
646 'getAttempt' => 0,
647 'getMaxPoints' => 25,
648 'getReachedPoints' => 25,
649 'getMarkShort' => 'passed',
650 'getMarkOfficial' => 'passed',
651 'getTimestamp' => 1740557748,
652 ]
653 ]
654 ];
655 }
656
664 public static function provideTestAttemptResult(): array
665 {
666 return [
667 [
668 [
669 'active_fi' => 10,
670 'pass' => 0,
671 'points' => 0,
672 'maxpoints' => 25,
673 'questioncount' => 3,
674 'answeredquestions' => 2,
675 'workingtime' => 12,
676 'tstamp' => 1740557748,
677 'exam_id' => 'I0_T334_A41_P0',
678 'finalized_by' => null,
679 ],
680 [
681 'getActiveId' => 10,
682 'getAttempt' => 0,
683 'getMaxPoints' => 25,
684 'getReachedPoints' => 0,
685 'getQuestionCount' => 3,
686 'getAnsweredQuestions' => 2,
687 'getWorkingTime' => 12,
688 'getTimestamp' => 1740557748,
689 'getExamId' => 'I0_T334_A41_P0',
690 'getFinalizedBy' => null
691 ]
692 ],
693 ];
694 }
695
706 public static function provideFetchedTestResult(): array
707 {
708 return [
709 [
710 // Test Parameters
711 [
712 'active_id' => 10,
713 'pass' => 0,
714 'test_obj_id' => 100,
715 ],
716 // Results of query 1
717 [
718 'pass' => 0,
719 'points' => 10,
720 'answeredquestions' => 2,
721 ],
722 // Result of query 2
723 [
724 'qcount' => 3,
725 'qsum' => 25
726 ],
727 // Result of query 3
728 [
729 'started' => '2024-01-01 04:05:05',
730 'finished' => '2024-01-01 04:05:17'
731 ],
732 // Expected Results
733 [
734 'getActiveId' => 10,
735 'getAttempt' => 0,
736 'getMaxPoints' => 25,
737 'getReachedPoints' => 10,
738 'getQuestionCount' => 3,
739 'getAnsweredQuestions' => 2,
740 'getWorkingTime' => 12,
741 'getExamId' => 'I_T100_A10_P0', // see \ilObjTest::buildExamId
742 'getFinalizedBy' => null
743 ],
744 ]
745 ];
746 }
747}
Builds data types.
Definition: Factory.php:36
Class ParticipantResult is a model representation of an entry in the test_result_cache table.
A class defining mark schemas for assessment test objects.
Definition: MarkSchema.php:37
A class defining marks for assessment test objects.
Definition: Mark.php:37
testGetTestAttemptResult(array $query_result, array $expected)
testRemoveTestResults(array $query, array $cached_status, array $expected)
mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once=false)
mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time)
testReadFromCache(array $query, array $cached_status, array $expected)
mockGetResultQuery(string $table, ?array $fetch_assoc_return)
testGetPassedParticipants(int $test_obj_id, array $query_result)
static provideFetchedTestResult()
This method returns sample data for these queries:
static provideTestAttemptResult()
This method returns sample data for this query:
static provideFetchedTestAttemptResult()
This method returns sample data for this query:
testUpdateTestAttemptResult(array $parameters, array $test_result, array $test_config_result, array $working_time_result, array $expected)
static provideCachedStatus()
This method returns test parameter, sample data and expected results for testing status cache.
static providePassedParticipants()
This method returns sample data for this query:
static provideTestResultCache()
This method returns sample data for this query:
__construct()
Constructor setup ILIAS global object @access public.
Definition: class.ilias.php:76
const QUESTION_SET_TYPE_FIXED
Class ilTestBaseClass.
adaptDICServiceMock(string $service_name, callable $adapt)
A transformation is a function from one datatype to another.
Interface ilDBInterface.
has(string $class_name)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.
global $DIC
Definition: shib_login.php:26