ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
TestResultRepositoryTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
31use PHPUnit\Framework\MockObject\MockObject;
32
34{
35 public function testConstruct(): void
36 {
37 $repository = $this->createInstance();
38 $this->assertInstanceOf(Repository::class, $repository);
39 }
40
44 public function testGetPassedParticipants(int $test_obj_id, array $query_result): void
45 {
46 $this->mockGetPassedParticipants($query_result);
47 $repository = $this->createInstance();
48
49 $actual = $repository->getPassedParticipants($test_obj_id);
50 foreach ($actual as $index => $participant) {
51 $this->assertEquals($participant['active_id'], $query_result[$index]['active_id']);
52 $this->assertEquals($participant['user_id'], $query_result[$index]['user_id']);
53 }
54 }
55
59 public function testGetTestResult(array $query_result, array $expected): void
60 {
61 $this->mockGetTestResultQuery($query_result);
62 $repository = $this->createInstance($query_result);
63
64 $actual = $repository->getTestResult($query_result['active_fi']);
65
66 $this->assertNotNull($actual);
67 $this->assertInstanceOf(ParticipantResult::class, $actual);
68 foreach ($expected as $method => $value) {
69 $this->assertEquals($value, $actual->$method());
70 }
71 }
72
73 public function testGetTestResultNotFound(): void
74 {
75 $this->mockGetTestResultQuery(null);
76 $repository = $this->createInstance();
77
78 $actual = $repository->getTestResult(1000);
79
80 $this->assertNull($actual);
81 }
82
86 public function testReadStatus(array $query_result, array $expected): void
87 {
88 $this->mockUpdateTestResultCache($query_result);
89 $repository = $this->createInstance();
90 $repository->updateTestResultCache($query_result['active_fi']);
91
92 $user_id = $query_result['user_id'];
93 $test_obj_id = $query_result['test_obj_id'];
94
95 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
96 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
97 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
98 }
99
100 public function testFailedPassedNotFound(): void
101 {
102 $repository = $this->createInstance();
103
104 $this->assertFalse($repository->isPassed(100, 200));
105 $this->assertFalse($repository->isFailed(100, 200));
106 $this->assertFalse($repository->hasFinished(100, 200));
107 }
108
112 public function testReadFromCache(array $query, array $cached_status, array $expected): void
113 {
114 $repository = $this->createInstance();
115 $user_id = $query['user_id'];
116 $test_obj_id = $query['test_obj_id'];
117
118 // Ensure the data is queried from the database, as it is not yet in the cache
119 $this->mockReadResultStatusQuery($cached_status);
120
121 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
122 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
123 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
124
125 // Ensure the database is not queried again
126 $this->adaptDICServiceMock(
127 \ilDBInterface::class,
128 function (\ilDBInterface|MockObject $mock) {
129 $mock->expects($this->exactly(0))->method('queryF');
130 $mock->expects($this->exactly(0))->method('fetchAssoc');
131 }
132 );
133
134 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
135 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
136 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
137 }
138
142 public function testRemoveTestResults(array $query, array $cached_status, array $expected): void
143 {
144 $repository = $this->createInstance();
145 $user_id = $query['user_id'];
146 $active_id = $query['active_id'];
147 $test_obj_id = $query['test_obj_id'];
148
149 // Ensure the data is loaded from the database, as it is not yet in the cache
150 $this->mockReadResultStatusQuery($cached_status);
151
152 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
153 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
154 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
155
156 $this->mockGetUserIds([['user_fi' => $user_id]]);
157 $repository->removeTestResults([$active_id], $test_obj_id);
158
159 // Ensure the data is queried again
160 $this->mockReadResultStatusQuery($cached_status);
161
162 $this->assertEquals($expected['isPassed'], $repository->isPassed($user_id, $test_obj_id));
163 $this->assertEquals($expected['isFailed'], $repository->isFailed($user_id, $test_obj_id));
164 $this->assertEquals($expected['hasFinished'], $repository->hasFinished($user_id, $test_obj_id));
165 }
166
170 public function testGetTestAttemptResult(array $query_result, array $expected): void
171 {
172 $this->mockGetTestPassResultQuery($query_result);
173 $repository = $this->createInstance();
174
175 $actual = $repository->getTestAttemptResult($query_result['active_fi']);
176
177 $this->assertNotNull($actual);
178 $this->assertInstanceOf(AttemptResult::class, $actual);
179 foreach ($expected as $method => $value) {
180 $this->assertEquals($value, $actual->$method());
181 }
182 }
183
184 public function testGetTestAttemptResultNotFound(): void
185 {
186 $this->mockGetTestPassResultQuery(null);
187 $repository = $this->createInstance();
188
189 $actual = $repository->getTestAttemptResult(1000);
190
191 $this->assertNull($actual);
192 }
193
198 array $parameters,
199 array $test_result,
200 array $test_config_result,
201 array $working_time_result,
202 array $expected
203 ): void {
204 $this->mockUpdateTestAttemptResult($test_result, $test_config_result, $working_time_result);
205 $repository = $this->createInstance();
206
207 $actual = $repository->updateTestAttemptResult(
208 $parameters['active_id'],
209 $parameters['pass'],
210 null,
211 $parameters['test_obj_id'],
212 false
213 );
214
215 $this->assertNotNull($actual);
216 $this->assertInstanceOf(AttemptResult::class, $actual);
217 $this->assertEqualsWithDelta(time(), $actual->getTimestamp(), 5);
218 foreach ($expected as $method => $value) {
219 $this->assertEquals($value, $actual->$method());
220 }
221 }
222
223 /*
224 Mocking
225 */
226
227 private function createInstance(?array $mock_data = null): Repository
228 {
229 $global_cache = $this->createConfiguredMock(
230 \ILIAS\Cache\Services::class,
231 ['get' => $this->createCacheMock()]
232 );
233
234 return $this->createInstanceOf(
235 Repository::class,
236 ['marks_repository' => $this->createMarksRepositoryMock($mock_data), 'global_cache' => $global_cache]
237 );
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 MarkSchema(0))->createSimpleSchema();
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): void
268 {
269 throw new \Error('Not implemented');
270 }
271 };
272 }
273
274 private function createCacheMock(): Container
275 {
276 return new class () implements Container {
277 private array $cache = [];
278
279 public function lock(float $seconds): void
280 {
281 throw new \Error('Not implemented');
282 }
283
284 public function isLocked(): bool
285 {
286 throw new \Error('Not implemented');
287 }
288
289 public function has(string $key): bool
290 {
291 return isset($this->cache[$key]);
292 }
293
294 public function get(string $key, Transformation $transformation): string|int|array|bool|null
295 {
296 return $this->cache[$key] ?? null;
297 }
298
299 public function set(string $key, string|int|array|bool|null $value, ?int $ttl = null): void
300 {
301 $this->cache[$key] = $value;
302 }
303
304 public function delete(string $key): void
305 {
306 unset($this->cache[$key]);
307 }
308
309 public function flush(): void
310 {
311 $this->cache = [];
312 }
313
314 public function getAdaptorName(): string
315 {
316 throw new \Error('Not implemented');
317 }
318
319 public function getContainerName(): string
320 {
321 throw new \Error('Not implemented');
322 }
323 };
324 }
325
326 /*
327 Database Mocking
328 */
329
333 private function mockGetPassedParticipants(array $fetch_all_return): void
334 {
335 $this->adaptDICServiceMock(
336 \ilDBInterface::class,
337 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
338 $mock
339 ->expects($this->once())
340 ->method('queryF')
341 ->with($this->stringContains("WHERE tst_tests.obj_fi = %s AND tst_result_cache.passed_once = 1"));
342
343 $mock
344 ->expects($this->once())
345 ->method('fetchAll')
346 ->willReturn($fetch_all_return);
347 }
348 );
349 }
350
354 private function mockGetTestResultQuery(?array $fetch_assoc_return): void
355 {
356 if ($fetch_assoc_return) {
357 $fetch_assoc_return['test_id'] = 0;
358 }
359 $this->mockGetResultQuery('tst_result_cache', $fetch_assoc_return);
360 }
361
365 private function mockGetTestPassResultQuery(?array $fetch_assoc_return): void
366 {
367 $this->mockGetResultQuery('tst_pass_result', $fetch_assoc_return);
368 }
369
370 private function mockGetResultQuery(string $table, ?array $fetch_assoc_return): void
371 {
372 $this->adaptDICServiceMock(
373 \ilDBInterface::class,
374 function (\ilDBInterface|MockObject $mock) use ($table, $fetch_assoc_return) {
375 $mock
376 ->expects($this->once())
377 ->method('queryF');
378
379 $mock
380 ->expects($this->once())
381 ->method('fetchAssoc')
382 ->willReturn($fetch_assoc_return);
383 }
384 );
385 }
386
390 private function mockReadResultStatusQuery(?array $fetch_assoc_return): void
391 {
392 $this->adaptDICServiceMock(
393 \ilDBInterface::class,
394 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_return) {
395 $mock->expects($this->atLeastOnce())->method('queryF');
396 $mock->expects($this->atLeastOnce())->method('fetchAssoc')->willReturn($fetch_assoc_return);
397 }
398 );
399 }
400
404 private function mockGetUserIds(?array $fetch_all_return): void
405 {
406 $this->adaptDICServiceMock(
407 \ilDBInterface::class,
408 function (\ilDBInterface|MockObject $mock) use ($fetch_all_return) {
409 $mock->expects($this->once())
410 ->method('query')
411 ->with($this->equalTo("SELECT user_fi FROM tst_active WHERE\n"));
412 $mock->expects($this->once())->method('fetchAll')->willReturn($fetch_all_return);
413 }
414 );
415 }
416
420 private function mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once = false): void
421 {
422 $fetch_assoc_mocks = [
423 ['pass_scoring' => \ilObjTest::SCORE_LAST_PASS], // \ilObjTest::_getPassScoring
424 ['maxpass' => 0], // \ilObjTest::_getMaxPass
425 $test_attempt_result, // TestResultRepository::fetchTestPassResult
426 ];
427
428 $this->adaptDICServiceMock(
429 \ilDBInterface::class,
430 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_mocks) {
431 // Ensures that the check whether results are available is mocked
432 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
433 'numRows' => 1,
434 ]);
435 $mock->method('queryF')->willReturn($mocked_stmt);
436
437 $mock->expects($this->exactly(count($fetch_assoc_mocks)))
438 ->method('fetchAssoc')
439 ->willReturnOnConsecutiveCalls(...$fetch_assoc_mocks);
440
441 $mock->expects($this->exactly(1))->method('replace');
442 }
443 );
444 }
445
449 private function mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time): void
450 {
451 $fetch_assoc_mocks = [
452 $test_result, // TestResultRepository::fetchTestResult
453 ['question_set_type' => \ilObjTest::QUESTION_SET_TYPE_FIXED], // TestResultRepository::fetchAdditionalTestData (1)
454 $test_config, // TestResultRepository::fetchAdditionalTestData (2)
455 $working_time, // TestResultRepository::fetchWorkingTime
456 null // TestResultRepository::fetchWorkingTime (i2)
457 ];
458
459 $this->adaptDICServiceMock(
460 \ilDBInterface::class,
461 function (\ilDBInterface|MockObject $mock) use ($fetch_assoc_mocks) {
462 // Ensures that the check whether results are available is mocked
463 $mocked_stmt = $this->createConfiguredMock(\ilDBStatement::class, [
464 'numRows' => 1,
465 ]);
466 $mock->method('queryF')->willReturn($mocked_stmt);
467
468 $mock->expects($this->exactly(count($fetch_assoc_mocks)))
469 ->method('fetchAssoc')
470 ->willReturnOnConsecutiveCalls(...$fetch_assoc_mocks);
471
472 $mock->expects($this->exactly(1))->method('replace');
473 }
474 );
475 }
476
477 /*
478 Data Provider
479 */
480
484 public static function provideCachedStatus(): array
485 {
486 return [
487 [
488 ['user_id' => 1, 'test_obj_id' => 100, 'active_id' => 1000],
489 ['passed' => true, 'failed' => false, 'finished' => false],
490 ['isPassed' => true, 'isFailed' => false, 'hasFinished' => false],
491 ],
492 [
493 ['user_id' => 10, 'test_obj_id' => 100, 'active_id' => 1000],
494 ['passed' => false, 'failed' => true, 'finished' => false],
495 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => false],
496 ],
497 [
498 ['user_id' => 1, 'test_obj_id' => 250, 'active_id' => 1400],
499 ['passed' => false, 'failed' => true, 'finished' => true],
500 ['isPassed' => false, 'isFailed' => true, 'hasFinished' => true],
501 ]
502 ];
503 }
504
514 public static function providePassedParticipants(): array
515 {
516 return [
517 [
518 10,
519 [
520 ['user_id' => 1, 'active_id' => 100],
521 ['user_id' => 2, 'active_id' => 200],
522 ['user_id' => 3, 'active_id' => 101],
523 ['user_id' => 4, 'active_id' => 201],
524 ['user_id' => 5, 'active_id' => 0],
525 ]
526 ],
527 ];
528 }
529
537 public static function provideTestResultCache(): array
538 {
539 return [
540 // Dataset #1: failed result
541 [
542 [
543 'active_fi' => 10,
544 'pass' => 0,
545 'max_points' => 25,
546 'reached_points' => 0,
547 'mark_short' => 'failed',
548 'mark_official' => 'failed',
549 'passed' => 0,
550 'failed' => 1,
551 'tstamp' => 1740557748,
552 'passed_once' => 0
553 ],
554 [
555 'getActiveId' => 10,
556 'isPassed' => false,
557 'isPassedOnce' => false,
558 'isFailed' => true,
559 'getAttempt' => 0,
560 'getMaxPoints' => 25,
561 'getReachedPoints' => 0,
562 'getMarkShort' => 'failed',
563 'getMarkOfficial' => 'failed',
564 'getTimestamp' => 1740557748,
565 ]
566 ],
567 ];
568 }
569
580 public static function provideFetchedTestAttemptResult(): array
581 {
582 return [
583 // Dataset #1: failed result
584 [
585 [
586 'active_fi' => 10,
587 'pass' => 0,
588 'maxpoints' => 0,
589 'questioncount' => 2,
590 'answeredquestions' => 2,
591 'workingtime' => 12,
592 'tstamp' => 1740557748,
593 'exam_id' => 'I0_T334_A41_P0',
594 'finalized_by' => null,
595 'last_finished_pass' => 0,
596 'user_id' => 1,
597 'test_id' => 5,
598 'test_obj_id' => 100,
599 'max_points' => 25,
600 'reached_points' => 0,
601 ],
602 [
603 'getActiveId' => 10,
604 'isPassed' => false,
605 'isPassedOnce' => false,
606 'isFailed' => true,
607 'hasFinished' => true,
608 'getAttempt' => 0,
609 'getMaxPoints' => 25,
610 'getReachedPoints' => 0,
611 'getMarkShort' => 'failed',
612 'getMarkOfficial' => 'failed',
613 'getTimestamp' => 1740557748,
614 ]
615 ],
616 // Dataset #2: success result
617 [
618 [
619 'active_fi' => 11,
620 'pass' => 0,
621 'maxpoints' => 0,
622 'questioncount' => 2,
623 'answeredquestions' => 2,
624 'workingtime' => 12,
625 'tstamp' => 1740557748,
626 'exam_id' => 'I0_T334_A41_P0',
627 'finalized_by' => null,
628 'last_finished_pass' => 1,
629 'user_id' => 1,
630 'test_id' => 5,
631 'test_obj_id' => 100,
632 'max_points' => 25,
633 'reached_points' => 25,
634 ],
635 [
636 'getActiveId' => 11,
637 'isPassed' => true,
638 'isPassedOnce' => true,
639 'isFailed' => false,
640 'hasFinished' => true,
641 'getAttempt' => 0,
642 'getMaxPoints' => 25,
643 'getReachedPoints' => 25,
644 'getMarkShort' => 'passed',
645 'getMarkOfficial' => 'passed',
646 'getTimestamp' => 1740557748,
647 ]
648 ]
649 ];
650 }
651
659 public static function provideTestAttemptResult(): array
660 {
661 return [
662 [
663 [
664 'active_fi' => 10,
665 'pass' => 0,
666 'points' => 0,
667 'maxpoints' => 25,
668 'questioncount' => 3,
669 'answeredquestions' => 2,
670 'workingtime' => 12,
671 'tstamp' => 1740557748,
672 'exam_id' => 'I0_T334_A41_P0',
673 'finalized_by' => null,
674 ],
675 [
676 'getActiveId' => 10,
677 'getAttempt' => 0,
678 'getMaxPoints' => 25,
679 'getReachedPoints' => 0,
680 'getQuestionCount' => 3,
681 'getAnsweredQuestions' => 2,
682 'getWorkingTime' => 12,
683 'getTimestamp' => 1740557748,
684 'getExamId' => 'I0_T334_A41_P0',
685 'getFinalizedBy' => null
686 ]
687 ],
688 ];
689 }
690
701 public static function provideFetchedTestResult(): array
702 {
703 return [
704 [
705 // Test Parameters
706 [
707 'active_id' => 10,
708 'pass' => 0,
709 'test_obj_id' => 100,
710 ],
711 // Results of query 1
712 [
713 'pass' => 0,
714 'points' => 10,
715 'answeredquestions' => 2,
716 ],
717 // Result of query 2
718 [
719 'qcount' => 3,
720 'qsum' => 25
721 ],
722 // Result of query 3
723 [
724 'started' => '2024-01-01 04:05:05',
725 'finished' => '2024-01-01 04:05:17'
726 ],
727 // Expected Results
728 [
729 'getActiveId' => 10,
730 'getAttempt' => 0,
731 'getMaxPoints' => 25,
732 'getReachedPoints' => 10,
733 'getQuestionCount' => 3,
734 'getAnsweredQuestions' => 2,
735 'getWorkingTime' => 12,
736 'getExamId' => 'I_T100_A10_P0', // see \ilObjTest::buildExamId
737 'getFinalizedBy' => null
738 ],
739 ]
740 ];
741 }
742}
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:36
A class defining marks for assessment test objects.
Definition: Mark.php:36
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
const SCORE_LAST_PASS
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.