Fáze požadavku: 10. Pest testování & QA
Modul 9 · Lekce 3

Testování v Pest PHP: Rozbor našeho vlastního CurriculumIntegrityTest

💡 Shrnutí lekce

Osvojte si moderní testování v PHP 8.4 a Pest PHP. Ukážeme si, jak píšeme integrační testy zabraňující halucinacím a testujeme Livewire komponenty v CoursePlatformTest.

Moderní testování v Laravelu a Pest PHP

Automatizované testování je základním pilířem profesionálního softwarového inženýrství. V moderním ekosystému Laravelu 12 a PHP 8.4 se standardem stal testovací framework Pest PHP a objektové Feature testy postavené na základech PHPUnit.

V naší výukové platformě nehrají testy pouze roli běžného ověření funkčnosti, ale slouží jako architektonický štít proti halucinacím (Zero-Hallucination Policy).


Anatomie testu CurriculumIntegrityTest

Prozkoumejte soubor tests/Feature/CurriculumIntegrityTest.php. Všimněte si tří klíčových testovacích metod:

1. test_curriculum_registry_json_is_valid_and_all_anatomy_files_exist()

Tento test načte konfigurační registr kurzu .agents/memory/curriculum_registry.json, zkontroluje existenci všech 10 modulů a pro každou jednotlivou lekci dynamicky ověří:

  • Zda jsou vyplněny povinné identifikátory (slug, lessons).
  • Zda každý odkazovaný soubor v anatomy_files reálně existuje na disku v našem repozitáři.
  • Zda má soubor nenulový počet řádků ($node->lineCount > 0).

Pokud by jakýkoliv vývojář nebo AI subagent odkázal v lekci na neexistující třídu či vymyslel fiktivní soubor, test okamžitě selže s jasnou chybovou hláškou: „Halucinace odhalena: Soubor X v lekci Y v repozitáři neexistuje!“.

2. test_anatomy_inspector_blocks_sensitive_files_and_path_traversal()

Tento test prověřuje bezpečnostní vrstvu služby AnatomyInspectorService. Simuluje pokusy o útok typu Path Traversal (../../etc/passwd, ../.env, .git/config, storage/logs/laravel.log) a ověřuje, že inspektor tyto požadavky striktně odmítne a nevrátí jejich obsah studentovi.

3. test_seeded_curriculum_matches_registry_with_quizzes()

Tento test provede seeding celé databáze ($this->seed(DatabaseSeeder::class)) a porovná záznamy v tabulkách course_modules a course_lessons s JSON registrem. Dále ověří, že:

  • Každá lekce má textový obsah o minimální délce.
  • Každá lekce obsahuje minimálně 3 kvízové otázky.

Testování reaktivních komponent v CoursePlatformTest

V souboru tests/Feature/CoursePlatformTest.php testujeme reaktivní Livewire komponenty i koncové HTTP endpointy.

Díky knihovně Livewire Testing můžeme komponenty testovat bez nutnosti spouštět pomalý headless prohlížeč (jako Selenium či Playwright):

$lesson = CourseLesson::with('quizQuestions')->firstOrFail();

$component = Livewire::test(LessonQuiz::class, ['lesson' => $lesson]);

foreach ($lesson->quizQuestions as $question) {
    $component->call('selectOption', $question->id, $question->correct_option);
}

$component->call('submitQuiz')
    ->assertSet('submitted', true)
    ->assertSet('scorePercent', 100);

Tento test během několika milisekund nasimuluje výběr správných odpovědí ve formuláři, odeslání kvízu a ověří, že interní reaktivní stav komponenty dosáhl 100% skóre.


Role traitu RefreshDatabase

Trait RefreshDatabase zajišťuje, že každý test běží v dokonale čistém a izolovaném prostředí. Při prvním spuštění provede migrace a každý jednotlivý test obalí do databázové transakce. Po skončení testu se provede ROLLBACK, takže žádný test neovlivňuje data ostatních testů.

💻 Klíčová konstrukce z praxe
// tests/Feature/CurriculumIntegrityTest.php
namespace Tests\Feature;

use App\Models\CourseLesson;
use App\Models\CourseModule;
use App\Services\AnatomyInspectorService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CurriculumIntegrityTest extends TestCase
{
    use RefreshDatabase;

    // Test ověřuje existenci všech souborů osnovy na disku (Zero Hallucination)
    public function test_curriculum_registry_json_is_valid_and_all_anatomy_files_exist(): void
    {
        $registryPath = base_path('.agents/memory/curriculum_registry.json');
        $this->assertFileExists($registryPath);

        $registry = json_decode((string) file_get_contents($registryPath), true);
        $this->assertCount(10, $registry['modules'] ?? []);

        /** @var AnatomyInspectorService $inspector */
        $inspector = app(AnatomyInspectorService::class);

        foreach ($registry['modules'] as $moduleDef) {
            foreach ($moduleDef['lessons'] as $lessonDef) {
                foreach ($lessonDef['anatomy_files'] as $filePath) {
                    $node = $inspector->inspectFile($filePath);
                    $this->assertTrue(
                        $node->exists,
                        "Halucinace odhalena: Soubor '{$filePath}' v lekci '{$lessonDef['slug']}' v repozitáři neexistuje!"
                    );
                    $this->assertGreaterThan(0, $node->lineCount);
                }
            }
        }
    }
}

🎯 Ověření znalostí z lekce

Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.

Otázka 1 z 3

Jak trait RefreshDatabase zajišťuje izolaci mezi jednotlivými testy?

Otázka 2 z 3

Jak v našem projektu testujeme interaktivní komponentu Livewire 3 bez spouštění plného headless browseru?

Otázka 3 z 3

K jakému účelu slouží test test_anatomy_inspector_blocks_sensitive_files_and_path_traversal?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
2 soubory
Na co se v kódu zaměřit: V tests/Feature/CurriculumIntegrityTest.php prozkoumej test test_curriculum_registry_json_is_valid_and_all_anatomy_files_exist(), který načítá JSON registr a přes AnatomyInspectorService fyzicky kontroluje přítomnost každého souboru v projektu.
cesta: tests/Feature/CurriculumIntegrityTest.php
90 řádků · 3.4 KB
Soubor aplikační architektury v modulu Pest PHP Tests.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Tests\Feature;
6
7 use App\Models\CourseLesson;
8 use App\Models\CourseModule;
9 use App\Services\AnatomyInspectorService;
10 use Database\Seeders\DatabaseSeeder;
11 use Illuminate\Foundation\Testing\RefreshDatabase;
12 use Tests\TestCase;
13
14 class CurriculumIntegrityTest extends TestCase
15 {
16 use RefreshDatabase;
17
18 public function test_curriculum_registry_json_is_valid_and_all_anatomy_files_exist(): void
19 {
20 $registryPath = base_path('.agents/memory/curriculum_registry.json');
21 $this->assertFileExists($registryPath);
22
23 $registry = json_decode((string) file_get_contents($registryPath), true);
24 $this->assertIsArray($registry);
25 $this->assertCount(10, $registry['modules'] ?? []);
26
27 /** @var AnatomyInspectorService $inspector */
28 $inspector = app(AnatomyInspectorService::class);
29
30 foreach ($registry['modules'] as $moduleDef) {
31 $this->assertNotEmpty($moduleDef['slug']);
32 $this->assertNotEmpty($moduleDef['lessons']);
33
34 foreach ($moduleDef['lessons'] as $lessonDef) {
35 $this->assertNotEmpty($lessonDef['anatomy_files']);
36
37 foreach ($lessonDef['anatomy_files'] as $filePath) {
38 $node = $inspector->inspectFile($filePath);
39 $this->assertTrue(
40 $node->exists,
41 "Halucinace odhalena: Soubor '{$filePath}' v lekci '{$lessonDef['slug']}' v repozitáři neexistuje nebo není povolen!"
42 );
43 $this->assertGreaterThan(0, $node->lineCount);
44 }
45 }
46 }
47 }
48
49 public function test_anatomy_inspector_blocks_sensitive_files_and_path_traversal(): void
50 {
51 /** @var AnatomyInspectorService $inspector */
52 $inspector = app(AnatomyInspectorService::class);
53
54 $forbiddenPaths = [
55 '.env',
56 '../.env',
57 '../../etc/passwd',
58 '.git/config',
59 'vendor/autoload.php',
60 'storage/logs/laravel.log',
61 ];
62
63 foreach ($forbiddenPaths as $forbidden) {
64 $this->assertFalse($inspector->isAllowedPath($forbidden), "Cesta '{$forbidden}' musí být zablokována!");
65 $node = $inspector->inspectFile($forbidden);
66 $this->assertFalse($node->exists);
67 }
68 }
69
70 public function test_seeded_curriculum_matches_registry_with_quizzes(): void
71 {
72 $this->seed(DatabaseSeeder::class);
73
74 $registry = json_decode((string) file_get_contents(base_path('.agents/memory/curriculum_registry.json')), true);
75
76 $this->assertSame(count($registry['modules']), CourseModule::count());
77
78 foreach ($registry['modules'] as $moduleDef) {
79 $module = CourseModule::where('slug', $moduleDef['slug'])->first();
80 $this->assertNotNull($module, "Chybí modul '{$moduleDef['slug']}' v databázi.");
81
82 foreach ($moduleDef['lessons'] as $lessonDef) {
83 $lesson = CourseLesson::where('slug', $lessonDef['slug'])->first();
84 $this->assertNotNull($lesson, "Chybí lekce '{$lessonDef['slug']}' v databázi.");
85 $this->assertGreaterThan(150, strlen($lesson->content_markdown), "Lekce '{$lesson->slug}' má příliš krátký obsah.");
86 $this->assertGreaterThanOrEqual(3, $lesson->quizQuestions()->count(), "Lekce '{$lesson->slug}' musí mít alespoň 3 kvízové otázky.");
87 }
88 }
89 }
90 }
91
cesta: tests/Feature/CoursePlatformTest.php
74 řádků · 2.3 KB
Soubor aplikační architektury v modulu Pest PHP Tests.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Tests\Feature;
6
7 use App\Livewire\LessonQuiz;
8 use App\Models\CourseLesson;
9 use App\Models\CourseModule;
10 use App\Services\StudentProgressService;
11 use Database\Seeders\DatabaseSeeder;
12 use Illuminate\Foundation\Testing\RefreshDatabase;
13 use Livewire\Livewire;
14 use Tests\TestCase;
15
16 class CoursePlatformTest extends TestCase
17 {
18 use RefreshDatabase;
19
20 public function test_home_anatomy_and_health_endpoints_return_ok(): void
21 {
22 $this->seed(DatabaseSeeder::class);
23
24 $this->get('/up')->assertOk();
25 $this->get('/')->assertOk()->assertSee('Anatomie Laravelu');
26 $this->get('/anatomie')->assertOk();
27 $this->get('/api/status')->assertOk()->assertJsonPath('status', 'ok');
28 }
29
30 public function test_module_and_lesson_pages_render_with_live_anatomy(): void
31 {
32 $this->seed(DatabaseSeeder::class);
33
34 $modules = CourseModule::with('lessons')->orderBy('number')->get();
35 $this->assertCount(10, $modules);
36
37 foreach ($modules as $module) {
38 $this->get(route('course.module', $module))
39 ->assertOk()
40 ->assertSee($module->title);
41
42 foreach ($module->lessons as $lesson) {
43 $this->get(route('course.lesson', [$module, $lesson]))
44 ->assertOk()
45 ->assertSee($lesson->title);
46 }
47 }
48 }
49
50 public function test_student_profile_transfer_code_and_livewire_quiz_work(): void
51 {
52 $this->seed(DatabaseSeeder::class);
53
54 /** @var StudentProgressService $progressService */
55 $progressService = app(StudentProgressService::class);
56 $profile = $progressService->resolveProfile(null);
57
58 $this->assertNotEmpty($profile->transfer_code);
59
60 $lesson = CourseLesson::with('quizQuestions')->firstOrFail();
61
62 $component = Livewire::test(LessonQuiz::class, ['lesson' => $lesson]);
63 foreach ($lesson->quizQuestions as $question) {
64 $component->call('selectOption', $question->id, $question->correct_option);
65 }
66 $component->call('submitQuiz')
67 ->assertSet('submitted', true)
68 ->assertSet('scorePercent', 100);
69
70 $this->post(route('course.switch-profile'), [
71 'transfer_code' => $profile->transfer_code,
72 ])->assertRedirect(route('course.index'));
73 }
74 }
75
Zdroj: skutečné soubory této aplikace Plný průzkumník anatomie →