ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
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;
34
36{
37 public function testConstruct(): void
38 {
39 $repository = $this->createInstance();
40 $this->assertInstanceOf(Repository::class, $repository);
41 }
42
46 public function testGetPassedParticipants(int $test_obj_id, array $query_result): void
47 {
48 $this->mockGetPassedParticipants($query_result);
49 $repository = $this->createInstance();
50
51 $actual = $repository->getPassedParticipants($test_obj_id);
52 foreach ($actual as $index => $participant) {
53 $this->assertEquals($participant['active_id'], $query_result[$index]['active_id']);
54 $this->assertEquals($participant['user_id'], $query_result[$index]['user_id']);
55 }
56 }
57
61 public function testGetTestResult(array $query_result, array $expected): void
62 {
63 $this->mockGetTestResultQuery($query_result);
64 $repository = $this->createInstance($query_result);
65
66 $actual = $repository->getTestResult($query_result['active_fi']);
67
68 $this->assertNotNull($actual);
69 $this->assertInstanceOf(ParticipantResult::class, $actual);
70 foreach ($expected as $method => $value) {
71 $this->assertEquals($value, $actual->$method());
72 }
73 }
74
75 public function testGetTestResultNotFound(): void
76 {
77 $this->mockGetTestResultQuery(null);
78 $repository = $this->createInstance();
79
80 $actual = $repository->getTestResult(1000);
81
82 $this->assertNull($actual);
83 }
84
88 public function testReadStatus(array $query_result, array $expected): void
89 {
90 $this->mockUpdateTestResultCache($query_result);
91 $repository = $this->createInstance();
92 $repository->updateTestResultCache($query_result['active_fi']);
93
94 $user_id = $query_result['user_id'];
95 $test_obj_id = $query_result['test_obj_id'];
96
97 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
98 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
99 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
100 }
101
102 public function testFailedPassedNotFound(): void
103 {
104 $repository = $this->createInstance();
105
106 $this->assertFalse($repository->isPassed(100, 200));
107 $this->assertFalse($repository->isFailed(100, 200));
108 $this->assertFalse($repository->hasFinished(100, 200));
109 }
110
114 public function testReadFromCache(array $query, array $cached_status, array $expected): void
115 {
116 $repository = $this->createInstance();
117 $user_id = $query['user_id'];
118 $test_obj_id = $query['test_obj_id'];
119
120 // Ensure the data is queried from the database, as it is not yet in the cache
121 $this->mockReadResultStatusQuery($cached_status);
122
123 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
124 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
125 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
126
127 // Ensure the database is not queried again
128 $this->adaptDICServiceMock(
129 \ilDBInterface::class,
130 function (\ilDBInterface|MockObject $mock) {
131 $mock->expects($this->exactly(0))->method('queryF');
132 $mock->expects($this->exactly(0))->method('fetchAssoc');
133 }
134 );
135
136 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
137 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
138 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
139 }
140
144 public function testRemoveTestResults(array $query, array $cached_status, array $expected): void
145 {
146 $repository = $this->createInstance();
147 $user_id = $query['user_id'];
148 $active_id = $query['active_id'];
149 $test_obj_id = $query['test_obj_id'];
150
151 // Ensure the data is loaded from the database, as it is not yet in the cache
152 $this->mockReadResultStatusQuery($cached_status);
153
154 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
155 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
156 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
157
158 $this->mockGetUserIds([['user_fi' => $user_id]]);
159 $repository->removeTestResults([$active_id], $test_obj_id);
160
161 // Ensure the data is queried again
162 $this->mockReadResultStatusQuery($cached_status);
163
164 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
165 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
166 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
167 }
168
172 public function testGetTestAttemptResult(array $query_result, array $expected): void
173 {
174 $this->mockGetTestPassResultQuery($query_result);
175 $repository = $this->createInstance();
176
177 $actual = $repository->getTestAttemptResult($query_result['active_fi']);
178
179 $this->assertNotNull($actual);
180 $this->assertInstanceOf(AttemptResult::class, $actual);
181 foreach ($expected as $method => $value) {
182 $this->assertEquals($value, $actual->$method());
183 }
184 }
185
186 public function testGetTestAttemptResultNotFound(): void
187 {
188 $this->mockGetTestPassResultQuery(null);
189 $repository = $this->createInstance();
190
191 $actual = $repository->getTestAttemptResult(1000);
192
193 $this->assertNull($actual);
194 }
195
200 array $parameters,
201 array $test_result,
202 array $test_config_result,
203 array $working_time_result,
204 array $expected
205 ): void {
206 $this->mockUpdateTestAttemptResult($test_result, $test_config_result, $working_time_result);
207 $repository = $this->createInstance();
208
209 $actual = $repository->updateTestAttemptResult(
210 $parameters['active_id'],
211 $parameters['pass'],
212 null,
213 $parameters['test_obj_id'],
214 false
215 );
216
217 $this->assertNotNull($actual);
218 $this->assertInstanceOf(AttemptResult::class, $actual);
219 $this->assertEqualsWithDelta(time(), $actual->getTimestamp(), 5);
220 foreach ($expected as $method => $value) {
221 $this->assertEquals($value, $actual->$method());
222 }
223 }
224
225 /*
226 Mocking
227 */
228
229 private function createInstance(?array $mock_data = null): Repository
230 {
231 global $DIC;
232
233 $global_cache = $this->createConfiguredMock(
234 \ILIAS\Cache\Services::class,
235 ['get' => $this->createCacheMock()]
236 );
237
238 $partial_mock = $this->getMockBuilder(Repository::class)
239 ->disableOriginalClone()
240 ->disableArgumentCloning()
241 ->disallowMockingUnknownTypes()
242 ->setConstructorArgs([
243 $DIC->database(),
244 $this->createMock(Refinery::class),
245 $this->createMarksRepositoryMock($mock_data),
246 $global_cache
247 ])
248 ->onlyMethods(['lookupAttempt'])
249 ->getMock();
250 $partial_mock->method('lookupAttempt')->willReturn(0);
251
252 return $partial_mock;
253 }
254
255 private function createMarksRepositoryMock(?array $mock_data): MarksRepository
256 {
257 if ($mock_data) {
258 $mock = new Mark(
259 $mock_data['mark_short'],
260 $mock_data['mark_official'],
261 0.0,
262 (bool) $mock_data['passed']
263 );
264 $mark_schema = $this->createConfiguredMock(
265 MarkSchema::class,
266 ['getMatchingMark' => $mock]
267 );
268 } else {
269 $mark_schema = (new MarkSchemaFactory())->createSimpleSchema(0);
270 }
271
272 return new class ($mark_schema) implements MarksRepository {
273 public function __construct(protected MarkSchema $mark_schema)
274 {
275 }
276
277 public function getMarkSchemaFor(int $test_id): MarkSchema
278 {
279 return $this->mark_schema;
280 }
281
282 public function storeMarkSchema(MarkSchema $mark_schema): array
283 {
284 throw new \Error('Not implemented');
285 }
286
287 public function getMarkSchemaBySteps(array $step_ids): MarkSchema
288 {
289 throw new \Error('Not implemented');
290 }
291
292 public function deleteSteps(array $step_ids): void
293 {
294 throw new \Error('Not implemented');
295 }
296 };
297 }
298
299 private function createCacheMock(): Container
300 {
301 return new class () implements Container {
302 private array $cache = [];
303
304 public function lock(float $seconds): void
305 {
306 throw new \Error('Not implemented');
307 }
308
309 public function isLocked(): bool
310 {
311 throw new \Error('Not implemented');
312 }
313
314 public function has(string $key): bool
315 {
316 return isset($this->cache[$key]);
317 }
318
319 public function get(string $key, Transformation $transformation): string|int|array|bool|null
320 {
321 return $this->cache[$key] ?? null;
322 }
323
324 public function set(string $key, string|int|array|bool|null $value, ?int $ttl = null): void
325 {
326 $this->cache[$key] = $value;
327 }
328
329 public function delete(string $key): void
330 {
331 unset($this->cache[$key]);
332 }
333
334 public function flush(): void
335 {
336 $this->cache = [];
337 }
338
339 public function getAdaptorName(): string
340 {
341 throw new \Error('Not implemented');
342 }
343
344 public function getContainerName(): string
345 {
346 throw new \Error('Not implemented');
347 }
348 };
349 }
350
351 /*
352 Database Mocking
353 */
354
358 private function mockGetPassedParticipants(array $fetch_all_return): void
359 {
360 $this->adaptDICServiceMock(
361 \ilDBInterface::class,
362 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
363 $mock
364 ->expects($this->once())
365 ->method('queryF')
366 ->with($this->stringContains("WHERE tst_tests.obj_fi = %s AND tst_result_cache.passed_once = 1"));
367
368 $mock
369 ->expects($this->once())
370 ->method('fetchAll')
371 ->willReturn($fetch_all_return);
372 }
373 );
374 }
375
379 private function mockGetTestResultQuery(?array $fetch_assoc_return): void
380 {
381 if ($fetch_assoc_return) {
382 $fetch_assoc_return['test_id'] = 0;
383 }
384 $this->mockGetResultQuery('tst_result_cache', $fetch_assoc_return);
385 }
386
390 private function mockGetTestPassResultQuery(?array $fetch_assoc_return): void
391 {
392 $this->mockGetResultQuery('tst_pass_result', $fetch_assoc_return);
393 }
394
395 private function mockGetResultQuery(string $table, ?array $fetch_assoc_return): void
396 {
397 $this->adaptDICServiceMock(
398 \ilDBInterface::class,
399 function (\ilDBInterface|MockObject $mock) use ($table, $fetch_assoc_return) {
400 $mock
401 ->expects($this->once())
402 ->method('queryF');
403
404 $mock
405 ->expects($this->once())
406 ->method('fetchAssoc')
407 ->willReturn($fetch_assoc_return);
408 }
409 );
410 }
411
415 private function mockReadResultStatusQuery(?array $fetch_assoc_return): void
416 {
417 $this->adaptDICServiceMock(
418 \ilDBInterface::class,
419 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_return) {
420 $mock->expects($this->atLeastOnce())->method('queryF');
421 $mock->expects($this->atLeastOnce())->method('fetchAssoc')->willReturn($fetch_assoc_return);
422 }
423 );
424 }
425
429 private function mockGetUserIds(?array $fetch_all_return): void
430 {
431 $this->adaptDICServiceMock(
432 \ilDBInterface::class,
433 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
434 $mock->expects($this->once())
435 ->method('query')
436 ->with($this->equalTo("SELECT user_fi FROM tst_active WHERE\n"));
437 $mock->expects($this->once())->method('fetchAll')->willReturn($fetch_all_return);
438 }
439 );
440 }
441
445 private function mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once = false): void
446 {
447 $this->adaptDICServiceMock(
448 \ilDBInterface::class,
449 function (\ilDBInterface|MockObject $mock) use ($test_attempt_result) {
450 // Ensures that the check whether results are available is mocked
451 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
452 'numRows' => 1,
453 ]);
454 $mock->method('queryF')->willReturn($mocked_stmt);
455
456 // TestResultRepository::fetchTestPassResult
457 $mock->expects($this->exactly(1))
458 ->method('fetchAssoc')
459 ->willReturn($test_attempt_result);
460
461 $mock->expects($this->exactly(1))->method('replace');
462 }
463 );
464 }
465
469 private function mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time): void
470 {
471 $fetch_assoc_mocks = [
472 $test_result, // TestResultRepository::fetchTestResult
473 ['question_set_type' => \ilObjTest::QUESTION_SET_TYPE_FIXED], // TestResultRepository::fetchAdditionalTestData (1)
474 $test_config, // TestResultRepository::fetchAdditionalTestData (2)
475 $working_time, // TestResultRepository::fetchWorkingTime
476 null // TestResultRepository::fetchWorkingTime (i2)
477 ];
478
479 $this->adaptDICServiceMock(
480 \ilDBInterface::class,
481 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_mocks) {
482 // Ensures that the check whether results are available is mocked
483 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
484 'numRows' => 1,
485 ]);
486 $mock->method('queryF')->willReturn($mocked_stmt);
487
488 $mock->expects($this->exactly(count($fetch_assoc_mocks)))
489 ->method('fetchAssoc')
490 ->willReturnOnConsecutiveCalls(...$fetch_assoc_mocks);
491
492 $mock->expects($this->exactly(1))->method('replace');
493 }
494 );
495 }
496
497 /*
498 Data Provider
499 */
500
504 public static function provideCachedStatus(): array
505 {
506 return [
507 [
508 ['user_id' => 1, 'test_obj_id' => 100, 'active_id' => 1000],
509 ['passed' => true, 'failed' => false, 'finished' => false],
510 ['isPassed' => true, 'isFailed' => false, 'hasFinished' => false],
511 ],
512 [
513 ['user_id' => 10, 'test_obj_id' => 100, 'active_id' => 1000],
514 ['passed' => false, 'failed' => true, 'finished' => false],
515 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => false],
516 ],
517 [
518 ['user_id' => 1, 'test_obj_id' => 250, 'active_id' => 1400],
519 ['passed' => false, 'failed' => true, 'finished' => true],
520 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => true],
521 ]
522 ];
523 }
524
534 public static function providePassedParticipants(): array
535 {
536 return [
537 [
538 10,
539 [
540 ['user_id' => 1, 'active_id' => 100],
541 ['user_id' => 2, 'active_id' => 200],
542 ['user_id' => 3, 'active_id' => 101],
543 ['user_id' => 4, 'active_id' => 201],
544 ['user_id' => 5, 'active_id' => 0],
545 ]
546 ],
547 ];
548 }
549
557 public static function provideTestResultCache(): array
558 {
559 return [
560 // Dataset #1: failed result
561 [
562 [
563 'active_fi' => 10,
564 'pass' => 0,
565 'max_points' => 25,
566 'reached_points' => 0,
567 'mark_short' => 'failed',
568 'mark_official' => 'failed',
569 'passed' => 0,
570 'failed' => 1,
571 'tstamp' => 1740557748,
572 'passed_once' => 0
573 ],
574 [
575 'getActiveId' => 10,
576 'isPassed' => false,
577 'isPassedOnce' => false,
578 'isFailed' => true,
579 'getAttempt' => 0,
580 'getMaxPoints' => 25,
581 'getReachedPoints' => 0,
582 'getMarkShort' => 'failed',
583 'getMarkOfficial' => 'failed',
584 'getTimestamp' => 1740557748,
585 ]
586 ],
587 ];
588 }
589
600 public static function provideFetchedTestAttemptResult(): array
601 {
602 return [
603 // Dataset #1: failed result
604 [
605 [
606 'active_fi' => 10,
607 'pass' => 0,
608 'maxpoints' => 0,
609 'questioncount' => 2,
610 'answeredquestions' => 2,
611 'workingtime' => 12,
612 'tstamp' => 1740557748,
613 'exam_id' => 'I0_T334_A41_P0',
614 'finalized_by' => null,
615 'last_finished_pass' => 0,
616 'user_id' => 1,
617 'test_id' => 5,
618 'test_obj_id' => 100,
619 'max_points' => 25,
620 'reached_points' => 0,
621 ],
622 [
623 'getActiveId' => 10,
624 'isPassed' => false,
625 'isPassedOnce' => false,
626 'isFailed' => true,
627 'hasFinished' => true,
628 'getAttempt' => 0,
629 'getMaxPoints' => 25,
630 'getReachedPoints' => 0,
631 'getMarkShort' => 'failed',
632 'getMarkOfficial' => 'failed',
633 'getTimestamp' => 1740557748,
634 ]
635 ],
636 // Dataset #2: success result
637 [
638 [
639 'active_fi' => 11,
640 'pass' => 0,
641 'maxpoints' => 0,
642 'questioncount' => 2,
643 'answeredquestions' => 2,
644 'workingtime' => 12,
645 'tstamp' => 1740557748,
646 'exam_id' => 'I0_T334_A41_P0',
647 'finalized_by' => null,
648 'last_finished_pass' => 1,
649 'user_id' => 1,
650 'test_id' => 5,
651 'test_obj_id' => 100,
652 'max_points' => 25,
653 'reached_points' => 25,
654 ],
655 [
656 'getActiveId' => 11,
657 'isPassed' => true,
658 'isPassedOnce' => true,
659 'isFailed' => false,
660 'hasFinished' => true,
661 'getAttempt' => 0,
662 'getMaxPoints' => 25,
663 'getReachedPoints' => 25,
664 'getMarkShort' => 'passed',
665 'getMarkOfficial' => 'passed',
666 'getTimestamp' => 1740557748,
667 ]
668 ]
669 ];
670 }
671
679 public static function provideTestAttemptResult(): array
680 {
681 return [
682 [
683 [
684 'active_fi' => 10,
685 'pass' => 0,
686 'points' => 0,
687 'maxpoints' => 25,
688 'questioncount' => 3,
689 'answeredquestions' => 2,
690 'workingtime' => 12,
691 'tstamp' => 1740557748,
692 'exam_id' => 'I0_T334_A41_P0',
693 'finalized_by' => null,
694 ],
695 [
696 'getActiveId' => 10,
697 'getAttempt' => 0,
698 'getMaxPoints' => 25,
699 'getReachedPoints' => 0,
700 'getQuestionCount' => 3,
701 'getAnsweredQuestions' => 2,
702 'getWorkingTime' => 12,
703 'getTimestamp' => 1740557748,
704 'getExamId' => 'I0_T334_A41_P0',
705 'getFinalizedBy' => null
706 ]
707 ],
708 ];
709 }
710
721 public static function provideFetchedTestResult(): array
722 {
723 return [
724 [
725 // Test Parameters
726 [
727 'active_id' => 10,
728 'pass' => 0,
729 'test_obj_id' => 100,
730 ],
731 // Results of query 1
732 [
733 'pass' => 0,
734 'points' => 10,
735 'answeredquestions' => 2,
736 ],
737 // Result of query 2
738 [
739 'qcount' => 3,
740 'qsum' => 25
741 ],
742 // Result of query 3
743 [
744 'started' => '2024-01-01 04:05:05',
745 'finished' => '2024-01-01 04:05:17'
746 ],
747 // Expected Results
748 [
749 'getActiveId' => 10,
750 'getAttempt' => 0,
751 'getMaxPoints' => 25,
752 'getReachedPoints' => 10,
753 'getQuestionCount' => 3,
754 'getAnsweredQuestions' => 2,
755 'getWorkingTime' => 12,
756 'getExamId' => 'I_T100_A10_P0', // see \ilObjTest::buildExamId
757 'getFinalizedBy' => null
758 ],
759 ]
760 ];
761 }
762}
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)
@dataProvider provideTestAttemptResult
testGetTestResult(array $query_result, array $expected)
@dataProvider provideTestResultCache
testRemoveTestResults(array $query, array $cached_status, array $expected)
@dataProvider provideCachedStatus
mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once=false)
mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time)
testReadStatus(array $query_result, array $expected)
@dataProvider provideFetchedTestAttemptResult
testReadFromCache(array $query, array $cached_status, array $expected)
@dataProvider provideCachedStatus
mockGetResultQuery(string $table, ?array $fetch_assoc_return)
testGetPassedParticipants(int $test_obj_id, array $query_result)
@dataProvider providePassedParticipants
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)
@dataProvider provideFetchedTestResult
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