ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
TestResultRepositoryTest.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
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  {
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  {
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 }
static provideFetchedTestAttemptResult()
This method returns sample data for this query:
A class defining mark schemas for assessment test objects.
Definition: MarkSchema.php:35
createInstanceOf(string $class_name, array $explicit_parameters=[])
mockUpdateTestAttemptResult(?array $test_result, ?array $test_config, ?array $working_time)
A class defining marks for assessment test objects.
Definition: Mark.php:35
Interface Observer Contains several chained tasks and infos about them.
mockUpdateTestResultCache(?array $test_attempt_result, bool $passed_once=false)
static provideCachedStatus()
This method returns test parameter, sample data and expected results for testing status cache...
mockGetResultQuery(string $table, ?array $fetch_assoc_return)
testGetTestResult(array $query_result, array $expected)
provideTestResultCache
has(string $class_name)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
adaptDICServiceMock(string $service_name, callable $adapt)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
__construct()
Constructor setup ILIAS global object public.
Definition: class.ilias.php:76
const SCORE_LAST_PASS
testUpdateTestAttemptResult(array $parameters, array $test_result, array $test_config_result, array $working_time_result, array $expected)
provideFetchedTestResult
static provideTestAttemptResult()
This method returns sample data for this query:
static provideTestResultCache()
This method returns sample data for this query:
testReadStatus(array $query_result, array $expected)
provideFetchedTestAttemptResult
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
testReadFromCache(array $query, array $cached_status, array $expected)
provideCachedStatus
Class ilTestBaseClass.
testGetPassedParticipants(int $test_obj_id, array $query_result)
providePassedParticipants
const QUESTION_SET_TYPE_FIXED
A transformation is a function from one datatype to another.
testRemoveTestResults(array $query, array $cached_status, array $expected)
provideCachedStatus
static providePassedParticipants()
This method returns sample data for this query:
testGetTestAttemptResult(array $query_result, array $expected)
provideTestAttemptResult
static provideFetchedTestResult()
This method returns sample data for these queries: