@the-bearded-bear/claude-craft 8.8.1 → 8.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dev/i18n/de/Angular/commands/check-compliance.md +218 -168
- package/Dev/i18n/de/Common/agents/refactoring-specialist.md +146 -69
- package/Dev/i18n/de/Common/commands/add-technology.md +166 -68
- package/Dev/i18n/de/Common/commands/setup-ci.md +216 -33
- package/Dev/i18n/de/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/de/Python/checklists/new-feature.md +319 -44
- package/Dev/i18n/de/React/rules/02-architecture.md +483 -322
- package/Dev/i18n/de/React/rules/03-coding-standards.md +584 -368
- package/Dev/i18n/de/Symfony/commands/check-compliance.md +186 -114
- package/Dev/i18n/de/Symfony/rules/07-testing-symfony.md +679 -10
- package/Dev/i18n/de/UIUX/commands/a11y-component.md +284 -26
- package/Dev/i18n/de/UIUX/commands/design-tokens.md +199 -35
- package/Dev/i18n/de/UIUX/commands/user-flow.md +179 -16
- package/Dev/i18n/de/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/es/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/es/Common/commands/add-technology.md +158 -60
- package/Dev/i18n/es/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/es/React/commands/check-testing.md +263 -169
- package/Dev/i18n/es/ReactNative/commands/check-testing.md +256 -198
- package/Dev/i18n/es/Symfony/commands/check-compliance.md +189 -117
- package/Dev/i18n/es/Symfony/commands/check-testing.md +301 -102
- package/Dev/i18n/es/Symfony/commands/migration-plan.md +267 -34
- package/Dev/i18n/es/UIUX/commands/a11y-component.md +274 -16
- package/Dev/i18n/es/UIUX/commands/design-tokens.md +186 -22
- package/Dev/i18n/es/UIUX/commands/user-flow.md +173 -10
- package/Dev/i18n/es/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/fr/Angular/commands/check-compliance.md +217 -167
- package/Dev/i18n/fr/Common/commands/ralph-run.md +138 -60
- package/Dev/i18n/fr/Laravel/commands/check-compliance.md +223 -115
- package/Dev/i18n/fr/React/commands/check-architecture.md +370 -53
- package/Dev/i18n/fr/React/commands/check-code-quality.md +454 -64
- package/Dev/i18n/fr/React/commands/check-testing.md +476 -102
- package/Dev/i18n/fr/Symfony/commands/check-compliance.md +188 -116
- package/Dev/i18n/fr/VueJS/commands/check-compliance.md +227 -102
- package/Dev/i18n/pt/Angular/commands/check-compliance.md +225 -175
- package/Dev/i18n/pt/Common/agents/ralph-conductor.md +211 -30
- package/Dev/i18n/pt/Common/commands/add-technology.md +149 -51
- package/Dev/i18n/pt/Common/commands/ralph-run.md +239 -35
- package/Dev/i18n/pt/Flutter/commands/analyze-performance.md +331 -46
- package/Dev/i18n/pt/Flutter/commands/check-compliance.md +234 -29
- package/Dev/i18n/pt/Flutter/commands/generate-feature.md +526 -51
- package/Dev/i18n/pt/Flutter/commands/generate-widget.md +515 -27
- package/Dev/i18n/pt/Flutter/commands/golden-update.md +322 -46
- package/Dev/i18n/pt/Flutter/commands/localization-check.md +278 -37
- package/Dev/i18n/pt/Laravel/commands/check-compliance.md +212 -104
- package/Dev/i18n/pt/React/commands/accessibility-check.md +151 -42
- package/Dev/i18n/pt/React/commands/bundle-analyze.md +300 -25
- package/Dev/i18n/pt/React/commands/check-architecture.md +369 -34
- package/Dev/i18n/pt/React/commands/check-code-quality.md +440 -34
- package/Dev/i18n/pt/React/commands/check-compliance.md +232 -62
- package/Dev/i18n/pt/React/commands/check-security.md +370 -31
- package/Dev/i18n/pt/React/commands/check-testing.md +473 -43
- package/Dev/i18n/pt/React/commands/generate-component.md +454 -19
- package/Dev/i18n/pt/React/commands/generate-hook.md +425 -24
- package/Dev/i18n/pt/React/commands/storybook-story.md +520 -35
- package/Dev/i18n/pt/ReactNative/commands/app-size.md +358 -387
- package/Dev/i18n/pt/ReactNative/commands/check-code-quality.md +246 -75
- package/Dev/i18n/pt/ReactNative/commands/check-compliance.md +191 -85
- package/Dev/i18n/pt/ReactNative/commands/check-security.md +363 -115
- package/Dev/i18n/pt/ReactNative/commands/check-testing.md +269 -79
- package/Dev/i18n/pt/ReactNative/commands/generate-screen.md +491 -324
- package/Dev/i18n/pt/ReactNative/commands/native-module.md +539 -82
- package/Dev/i18n/pt/ReactNative/commands/store-prepare.md +429 -185
- package/Dev/i18n/pt/ReactNative/rules/02-architecture.md +1084 -31
- package/Dev/i18n/pt/Symfony/commands/check-compliance.md +195 -123
- package/Dev/i18n/pt/UIUX/commands/a11y-audit.md +208 -24
- package/Dev/i18n/pt/UIUX/commands/a11y-component.md +281 -23
- package/Dev/i18n/pt/UIUX/commands/design-tokens.md +197 -33
- package/Dev/i18n/pt/UIUX/commands/user-flow.md +183 -20
- package/Dev/i18n/pt/VueJS/commands/check-compliance.md +228 -103
- package/Dev/i18n/pt/Workflow/commands/analyze.md +147 -146
- package/Dev/i18n/pt/Workflow/commands/design.md +205 -204
- package/Dev/i18n/pt/Workflow/commands/implement.md +184 -184
- package/Project/i18n/de/Sprint/commands/dev.md +339 -86
- package/Project/i18n/es/Sprint/commands/dev.md +342 -91
- package/Project/i18n/pt/Sprint/commands/dev.md +343 -92
- package/bundles/cursor/.cursorrules +1 -1
- package/bundles/windsurf/.windsurfrules +1 -1
- package/package.json +1 -1
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Überblick
|
|
4
4
|
|
|
5
|
-
Die **TDD (Test-Driven Development)**
|
|
5
|
+
Die **TDD-Entwicklung (Test-Driven Development)** ist für das gesamte Projekt Atoll Tourisme **OBLIGATORISCH**. Der Zyklus RED → GREEN → REFACTOR muss strikt eingehalten werden.
|
|
6
6
|
|
|
7
7
|
**Ziele:**
|
|
8
|
-
- ✅
|
|
9
|
-
- ✅ Mutation Score (Infection): **80
|
|
8
|
+
- ✅ Codeabdeckung: **mindestens 80 %**
|
|
9
|
+
- ✅ Mutation Score (Infection): **mindestens 80 %**
|
|
10
10
|
- ✅ Tests vor Implementierung (striktes TDD)
|
|
11
|
-
- ✅ BDD mit Behat für
|
|
11
|
+
- ✅ BDD mit Behat für fachliche Szenarien
|
|
12
12
|
|
|
13
13
|
> **Referenzen:**
|
|
14
14
|
> - `06-docker-hadolint.md` - Docker-Befehle für Tests
|
|
@@ -66,13 +66,13 @@ Die **TDD (Test-Driven Development)** Entwicklung ist **OBLIGATORISCH** für das
|
|
|
66
66
|
|
|
67
67
|
### Strikte TDD-Regeln
|
|
68
68
|
|
|
69
|
-
1. **Niemals Produktionscode ohne
|
|
69
|
+
1. **Niemals Produktionscode ohne vorher fehlschlagenden Test**
|
|
70
70
|
2. **Unit-Tests für Geschäftslogik (Domain)**
|
|
71
71
|
3. **Integrationstests für Repositories (Infrastructure)**
|
|
72
72
|
4. **Funktionale Tests für Use Cases (Application)**
|
|
73
|
-
5. **BDD/Behat für
|
|
73
|
+
5. **BDD/Behat für fachliche Benutzerszenarien**
|
|
74
74
|
|
|
75
|
-
### TDD
|
|
75
|
+
### Beispiel TDD: Money Value Object
|
|
76
76
|
|
|
77
77
|
#### 1. RED - Fehlschlagender Test
|
|
78
78
|
|
|
@@ -420,7 +420,7 @@ tests/
|
|
|
420
420
|
</phpunit>
|
|
421
421
|
```
|
|
422
422
|
|
|
423
|
-
###
|
|
423
|
+
### Testbefehle
|
|
424
424
|
|
|
425
425
|
```bash
|
|
426
426
|
# Alle Tests
|
|
@@ -438,12 +438,681 @@ make test-functional
|
|
|
438
438
|
# Mit Coverage
|
|
439
439
|
make test-coverage
|
|
440
440
|
|
|
441
|
-
# HTML-Datei
|
|
441
|
+
# Generierte HTML-Datei unter: var/coverage/html/index.html
|
|
442
442
|
```
|
|
443
443
|
|
|
444
444
|
---
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
## Unit-Tests
|
|
447
|
+
|
|
448
|
+
### Eigenschaften
|
|
449
|
+
|
|
450
|
+
- ✅ **Schnell** (< 100 ms pro Test)
|
|
451
|
+
- ✅ **Isoliert** (keine Datenbank, kein Netzwerk)
|
|
452
|
+
- ✅ **Deterministisch** (immer dasselbe Ergebnis)
|
|
453
|
+
- ✅ **Unabhängig** (zufällige Ausführungsreihenfolge)
|
|
454
|
+
|
|
455
|
+
### Beispiel: ReservationTest
|
|
456
|
+
|
|
457
|
+
```php
|
|
458
|
+
<?php
|
|
459
|
+
|
|
460
|
+
namespace App\Tests\Unit\Domain\Reservation\Entity;
|
|
461
|
+
|
|
462
|
+
use App\Domain\Reservation\Entity\Reservation;
|
|
463
|
+
use App\Domain\Reservation\Entity\Participant;
|
|
464
|
+
use App\Domain\Reservation\ValueObject\ReservationId;
|
|
465
|
+
use App\Domain\Reservation\ValueObject\Money;
|
|
466
|
+
use App\Domain\Reservation\ValueObject\ReservationStatus;
|
|
467
|
+
use App\Domain\Shared\ValueObject\Email;
|
|
468
|
+
use PHPUnit\Framework\TestCase;
|
|
469
|
+
|
|
470
|
+
final class ReservationTest extends TestCase
|
|
471
|
+
{
|
|
472
|
+
/**
|
|
473
|
+
* @test
|
|
474
|
+
*/
|
|
475
|
+
public function it_creates_a_reservation(): void
|
|
476
|
+
{
|
|
477
|
+
// Given
|
|
478
|
+
$id = ReservationId::generate();
|
|
479
|
+
$email = Email::fromString('client@example.com');
|
|
480
|
+
$montant = Money::fromEuros(500);
|
|
481
|
+
|
|
482
|
+
// When
|
|
483
|
+
$reservation = Reservation::create($id, $email, $montant);
|
|
484
|
+
|
|
485
|
+
// Then
|
|
486
|
+
self::assertEquals($id, $reservation->getId());
|
|
487
|
+
self::assertEquals($email, $reservation->getClientEmail());
|
|
488
|
+
self::assertEquals($montant, $reservation->getMontantTotal());
|
|
489
|
+
self::assertEquals(ReservationStatus::EN_ATTENTE, $reservation->getStatut());
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @test
|
|
494
|
+
*/
|
|
495
|
+
public function it_adds_a_participant(): void
|
|
496
|
+
{
|
|
497
|
+
// Given
|
|
498
|
+
$reservation = $this->createReservation();
|
|
499
|
+
$participant = $this->createParticipant('Jean Dupont', 30);
|
|
500
|
+
|
|
501
|
+
// When
|
|
502
|
+
$reservation->addParticipant($participant);
|
|
503
|
+
|
|
504
|
+
// Then
|
|
505
|
+
self::assertCount(1, $reservation->getParticipants());
|
|
506
|
+
self::assertContains($participant, $reservation->getParticipants());
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @test
|
|
511
|
+
*/
|
|
512
|
+
public function it_cannot_add_more_than_10_participants(): void
|
|
513
|
+
{
|
|
514
|
+
// Given
|
|
515
|
+
$reservation = $this->createReservation();
|
|
516
|
+
|
|
517
|
+
for ($i = 0; $i < 10; $i++) {
|
|
518
|
+
$reservation->addParticipant($this->createParticipant("Participant $i", 25));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Expect
|
|
522
|
+
$this->expectException(InvalidReservationException::class);
|
|
523
|
+
$this->expectExceptionMessage('Maximum 10 participants');
|
|
524
|
+
|
|
525
|
+
// When
|
|
526
|
+
$reservation->addParticipant($this->createParticipant('Participant 11', 25));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @test
|
|
531
|
+
*/
|
|
532
|
+
public function it_confirms_a_reservation(): void
|
|
533
|
+
{
|
|
534
|
+
// Given
|
|
535
|
+
$reservation = $this->createReservation();
|
|
536
|
+
$reservation->addParticipant($this->createParticipant('Jean', 30));
|
|
537
|
+
|
|
538
|
+
// When
|
|
539
|
+
$reservation->confirmer();
|
|
540
|
+
|
|
541
|
+
// Then
|
|
542
|
+
self::assertEquals(ReservationStatus::CONFIRMEE, $reservation->getStatut());
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* @test
|
|
547
|
+
*/
|
|
548
|
+
public function it_cannot_confirm_without_participants(): void
|
|
549
|
+
{
|
|
550
|
+
// Given
|
|
551
|
+
$reservation = $this->createReservation();
|
|
552
|
+
|
|
553
|
+
// Expect
|
|
554
|
+
$this->expectException(InvalidReservationException::class);
|
|
555
|
+
$this->expectExceptionMessage('At least one participant required');
|
|
556
|
+
|
|
557
|
+
// When
|
|
558
|
+
$reservation->confirmer();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @test
|
|
563
|
+
*/
|
|
564
|
+
public function it_cannot_confirm_a_cancelled_reservation(): void
|
|
565
|
+
{
|
|
566
|
+
// Given
|
|
567
|
+
$reservation = $this->createReservation();
|
|
568
|
+
$reservation->addParticipant($this->createParticipant('Jean', 30));
|
|
569
|
+
$reservation->annuler('Client request');
|
|
570
|
+
|
|
571
|
+
// Expect
|
|
572
|
+
$this->expectException(InvalidReservationException::class);
|
|
573
|
+
$this->expectExceptionMessage('Cannot confirm cancelled reservation');
|
|
574
|
+
|
|
575
|
+
// When
|
|
576
|
+
$reservation->confirmer();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @test
|
|
581
|
+
*/
|
|
582
|
+
public function it_cancels_a_reservation(): void
|
|
583
|
+
{
|
|
584
|
+
// Given
|
|
585
|
+
$reservation = $this->createReservation();
|
|
586
|
+
$raison = 'Client changed plans';
|
|
587
|
+
|
|
588
|
+
// When
|
|
589
|
+
$reservation->annuler($raison);
|
|
590
|
+
|
|
591
|
+
// Then
|
|
592
|
+
self::assertEquals(ReservationStatus::ANNULEE, $reservation->getStatut());
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* @test
|
|
597
|
+
*/
|
|
598
|
+
public function it_records_domain_events(): void
|
|
599
|
+
{
|
|
600
|
+
// Given
|
|
601
|
+
$reservation = $this->createReservation();
|
|
602
|
+
$reservation->addParticipant($this->createParticipant('Jean', 30));
|
|
603
|
+
|
|
604
|
+
// When
|
|
605
|
+
$reservation->confirmer();
|
|
606
|
+
|
|
607
|
+
// Then
|
|
608
|
+
$events = $reservation->pullDomainEvents();
|
|
609
|
+
self::assertCount(1, $events);
|
|
610
|
+
self::assertInstanceOf(ReservationConfirmedEvent::class, $events[0]);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Hilfsmethoden
|
|
614
|
+
private function createReservation(): Reservation
|
|
615
|
+
{
|
|
616
|
+
return Reservation::create(
|
|
617
|
+
ReservationId::generate(),
|
|
618
|
+
Email::fromString('test@example.com'),
|
|
619
|
+
Money::fromEuros(500)
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private function createParticipant(string $nom, int $age): Participant
|
|
624
|
+
{
|
|
625
|
+
return Participant::create(
|
|
626
|
+
ParticipantId::generate(),
|
|
627
|
+
PersonName::fromString($nom),
|
|
628
|
+
$age
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Integrationstests
|
|
637
|
+
|
|
638
|
+
### Eigenschaften
|
|
639
|
+
|
|
640
|
+
- ✅ Echte Datenbank (PostgreSQL im Test)
|
|
641
|
+
- ✅ Transaktionen werden nach jedem Test zurückgerollt
|
|
642
|
+
- ✅ Fixtures für Testdaten
|
|
643
|
+
- ✅ Tests der Doctrine-Repositories
|
|
644
|
+
|
|
645
|
+
### Beispiel: DoctrineReservationRepositoryTest
|
|
646
|
+
|
|
647
|
+
```php
|
|
648
|
+
<?php
|
|
649
|
+
|
|
650
|
+
namespace App\Tests\Integration\Infrastructure\Persistence\Doctrine\Repository;
|
|
651
|
+
|
|
652
|
+
use App\Domain\Reservation\Entity\Reservation;
|
|
653
|
+
use App\Domain\Reservation\ValueObject\ReservationId;
|
|
654
|
+
use App\Infrastructure\Persistence\Doctrine\Repository\DoctrineReservationRepository;
|
|
655
|
+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
656
|
+
|
|
657
|
+
final class DoctrineReservationRepositoryTest extends KernelTestCase
|
|
658
|
+
{
|
|
659
|
+
private DoctrineReservationRepository $repository;
|
|
660
|
+
|
|
661
|
+
protected function setUp(): void
|
|
662
|
+
{
|
|
663
|
+
self::bootKernel();
|
|
664
|
+
|
|
665
|
+
$this->repository = self::getContainer()->get(DoctrineReservationRepository::class);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* @test
|
|
670
|
+
*/
|
|
671
|
+
public function it_saves_and_finds_a_reservation(): void
|
|
672
|
+
{
|
|
673
|
+
// Given
|
|
674
|
+
$reservation = $this->createReservation();
|
|
675
|
+
|
|
676
|
+
// When
|
|
677
|
+
$this->repository->save($reservation);
|
|
678
|
+
|
|
679
|
+
// Entity Manager leeren, um frisches Laden aus der DB sicherzustellen
|
|
680
|
+
self::getContainer()->get('doctrine')->getManager()->clear();
|
|
681
|
+
|
|
682
|
+
$found = $this->repository->findById($reservation->getId());
|
|
683
|
+
|
|
684
|
+
// Then
|
|
685
|
+
self::assertNotNull($found);
|
|
686
|
+
self::assertEquals($reservation->getId(), $found->getId());
|
|
687
|
+
self::assertEquals($reservation->getClientEmail(), $found->getClientEmail());
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @test
|
|
692
|
+
*/
|
|
693
|
+
public function it_throws_exception_when_reservation_not_found(): void
|
|
694
|
+
{
|
|
695
|
+
// Given
|
|
696
|
+
$nonExistentId = ReservationId::generate();
|
|
697
|
+
|
|
698
|
+
// Expect
|
|
699
|
+
$this->expectException(ReservationNotFoundException::class);
|
|
700
|
+
|
|
701
|
+
// When
|
|
702
|
+
$this->repository->findById($nonExistentId);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* @test
|
|
707
|
+
*/
|
|
708
|
+
public function it_deletes_a_reservation(): void
|
|
709
|
+
{
|
|
710
|
+
// Given
|
|
711
|
+
$reservation = $this->createReservation();
|
|
712
|
+
$this->repository->save($reservation);
|
|
713
|
+
|
|
714
|
+
// When
|
|
715
|
+
$this->repository->delete($reservation);
|
|
716
|
+
|
|
717
|
+
// Then
|
|
718
|
+
$this->expectException(ReservationNotFoundException::class);
|
|
719
|
+
$this->repository->findById($reservation->getId());
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private function createReservation(): Reservation
|
|
723
|
+
{
|
|
724
|
+
return Reservation::create(
|
|
725
|
+
ReservationId::generate(),
|
|
726
|
+
Email::fromString('integration@test.com'),
|
|
727
|
+
Money::fromEuros(750)
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Funktionale Tests
|
|
736
|
+
|
|
737
|
+
### Eigenschaften
|
|
738
|
+
|
|
739
|
+
- ✅ Vollständige Tests der Use Cases
|
|
740
|
+
- ✅ Simulieren das Benutzerverhalten
|
|
741
|
+
- ✅ Überprüfen gesendete E-Mails
|
|
742
|
+
- ✅ Testen HTTP-Controller
|
|
743
|
+
|
|
744
|
+
### Beispiel: CreateReservationUseCaseTest
|
|
745
|
+
|
|
746
|
+
```php
|
|
747
|
+
<?php
|
|
748
|
+
|
|
749
|
+
namespace App\Tests\Functional\Application\Reservation\UseCase;
|
|
750
|
+
|
|
751
|
+
use App\Application\Reservation\UseCase\CreateReservation\CreateReservationCommand;
|
|
752
|
+
use App\Application\Reservation\UseCase\CreateReservation\CreateReservationUseCase;
|
|
753
|
+
use App\Domain\Reservation\Repository\ReservationRepositoryInterface;
|
|
754
|
+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
755
|
+
|
|
756
|
+
final class CreateReservationUseCaseTest extends KernelTestCase
|
|
757
|
+
{
|
|
758
|
+
private CreateReservationUseCase $useCase;
|
|
759
|
+
private ReservationRepositoryInterface $repository;
|
|
760
|
+
|
|
761
|
+
protected function setUp(): void
|
|
762
|
+
{
|
|
763
|
+
self::bootKernel();
|
|
764
|
+
|
|
765
|
+
$this->useCase = self::getContainer()->get(CreateReservationUseCase::class);
|
|
766
|
+
$this->repository = self::getContainer()->get(ReservationRepositoryInterface::class);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* @test
|
|
771
|
+
*/
|
|
772
|
+
public function it_creates_a_reservation(): void
|
|
773
|
+
{
|
|
774
|
+
// Given
|
|
775
|
+
$command = new CreateReservationCommand(
|
|
776
|
+
sejourId: 'sejour-123',
|
|
777
|
+
clientEmail: 'client@example.com',
|
|
778
|
+
participants: [
|
|
779
|
+
['nom' => 'Jean Dupont', 'age' => 30],
|
|
780
|
+
['nom' => 'Marie Dupont', 'age' => 28],
|
|
781
|
+
]
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// When
|
|
785
|
+
$reservationId = $this->useCase->execute($command);
|
|
786
|
+
|
|
787
|
+
// Then
|
|
788
|
+
$reservation = $this->repository->findById($reservationId);
|
|
789
|
+
self::assertNotNull($reservation);
|
|
790
|
+
self::assertCount(2, $reservation->getParticipants());
|
|
791
|
+
self::assertTrue($reservation->getMontantTotal()->isPositive());
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @test
|
|
796
|
+
*/
|
|
797
|
+
public function it_sends_confirmation_email(): void
|
|
798
|
+
{
|
|
799
|
+
// Given
|
|
800
|
+
$command = new CreateReservationCommand(
|
|
801
|
+
sejourId: 'sejour-123',
|
|
802
|
+
clientEmail: 'client@example.com',
|
|
803
|
+
participants: [['nom' => 'Jean', 'age' => 30]]
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// When
|
|
807
|
+
$this->useCase->execute($command);
|
|
808
|
+
|
|
809
|
+
// Then
|
|
810
|
+
self::assertEmailCount(1);
|
|
811
|
+
|
|
812
|
+
$email = self::getMailerMessage();
|
|
813
|
+
self::assertEmailAddressContains($email, 'to', 'client@example.com');
|
|
814
|
+
self::assertEmailHtmlBodyContains($email, 'Confirmation de réservation');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## BDD mit Behat
|
|
822
|
+
|
|
823
|
+
### Konfiguration behat.yml
|
|
824
|
+
|
|
825
|
+
```yaml
|
|
826
|
+
default:
|
|
827
|
+
suites:
|
|
828
|
+
reservation:
|
|
829
|
+
paths: ['%paths.base%/features/reservation']
|
|
830
|
+
contexts:
|
|
831
|
+
- App\Tests\Behat\Context\ReservationContext
|
|
832
|
+
|
|
833
|
+
extensions:
|
|
834
|
+
FriendsOfBehat\SymfonyExtension:
|
|
835
|
+
bootstrap: tests/bootstrap.php
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Feature: Reservierung erstellen
|
|
839
|
+
|
|
840
|
+
```gherkin
|
|
841
|
+
# features/reservation/create_reservation.feature
|
|
842
|
+
|
|
843
|
+
Feature: Créer une réservation
|
|
844
|
+
En tant que client
|
|
845
|
+
Je veux réserver un séjour
|
|
846
|
+
Afin de participer aux activités
|
|
847
|
+
|
|
848
|
+
Background:
|
|
849
|
+
Given les séjours suivants existent:
|
|
850
|
+
| id | titre | prix | date_debut | date_fin |
|
|
851
|
+
| sejour-ski | Séjour ski Alpes | 500€ | 2025-02-01 | 2025-02-07 |
|
|
852
|
+
| sejour-surf | Séjour surf Biarritz | 450€ | 2025-03-15 | 2025-03-22 |
|
|
853
|
+
|
|
854
|
+
Scenario: Créer une réservation avec 2 participants
|
|
855
|
+
When je crée une réservation pour le séjour "sejour-ski" avec:
|
|
856
|
+
| nom | age |
|
|
857
|
+
| Jean Dupont | 30 |
|
|
858
|
+
| Marie Dupont | 28 |
|
|
859
|
+
Then la réservation est créée
|
|
860
|
+
And le montant total est de "1000€"
|
|
861
|
+
And je reçois un email de confirmation
|
|
862
|
+
|
|
863
|
+
Scenario: Appliquer une remise famille nombreuse
|
|
864
|
+
When je crée une réservation pour le séjour "sejour-ski" avec:
|
|
865
|
+
| nom | age |
|
|
866
|
+
| Parent 1 | 35 |
|
|
867
|
+
| Parent 2 | 33 |
|
|
868
|
+
| Enfant 1 | 10 |
|
|
869
|
+
| Enfant 2 | 8 |
|
|
870
|
+
Then la réservation est créée
|
|
871
|
+
And une remise de "10%" est appliquée
|
|
872
|
+
And le montant total est de "1350€"
|
|
873
|
+
# Basis: (500 + 500 + 250 + 250) = 1500
|
|
874
|
+
# Rabatt 10 %: 1500 * 0.9 = 1350
|
|
875
|
+
|
|
876
|
+
Scenario: Refuser une réservation sans participant
|
|
877
|
+
When je crée une réservation pour le séjour "sejour-ski" sans participant
|
|
878
|
+
Then je reçois une erreur "At least one participant required"
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### Behat-Kontext
|
|
882
|
+
|
|
883
|
+
```php
|
|
884
|
+
<?php
|
|
885
|
+
|
|
886
|
+
namespace App\Tests\Behat\Context;
|
|
887
|
+
|
|
888
|
+
use App\Application\Reservation\UseCase\CreateReservation\CreateReservationCommand;
|
|
889
|
+
use App\Application\Reservation\UseCase\CreateReservation\CreateReservationUseCase;
|
|
890
|
+
use Behat\Behat\Context\Context;
|
|
891
|
+
use Behat\Gherkin\Node\TableNode;
|
|
892
|
+
use Symfony\Component\HttpKernel\KernelInterface;
|
|
893
|
+
|
|
894
|
+
final class ReservationContext implements Context
|
|
895
|
+
{
|
|
896
|
+
private ?string $reservationId = null;
|
|
897
|
+
private ?\Throwable $exception = null;
|
|
898
|
+
|
|
899
|
+
public function __construct(
|
|
900
|
+
private readonly KernelInterface $kernel,
|
|
901
|
+
) {}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* @Given les séjours suivants existent:
|
|
905
|
+
*/
|
|
906
|
+
public function lesSejursSuivantsExistent(TableNode $table): void
|
|
907
|
+
{
|
|
908
|
+
// Fixtures erstellen
|
|
909
|
+
foreach ($table->getHash() as $row) {
|
|
910
|
+
// Sejour-Fixtures erstellen...
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @When je crée une réservation pour le séjour :sejourId avec:
|
|
916
|
+
*/
|
|
917
|
+
public function jeCreeuneReservationPourLeSejourAvec(string $sejourId, TableNode $table): void
|
|
918
|
+
{
|
|
919
|
+
$participants = [];
|
|
920
|
+
|
|
921
|
+
foreach ($table->getHash() as $row) {
|
|
922
|
+
$participants[] = [
|
|
923
|
+
'nom' => $row['nom'],
|
|
924
|
+
'age' => (int) $row['age'],
|
|
925
|
+
];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
$command = new CreateReservationCommand(
|
|
929
|
+
sejourId: $sejourId,
|
|
930
|
+
clientEmail: 'behat@test.com',
|
|
931
|
+
participants: $participants
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
$useCase = $this->kernel->getContainer()->get(CreateReservationUseCase::class);
|
|
936
|
+
$this->reservationId = (string) $useCase->execute($command);
|
|
937
|
+
} catch (\Throwable $e) {
|
|
938
|
+
$this->exception = $e;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* @Then la réservation est créée
|
|
944
|
+
*/
|
|
945
|
+
public function laReservationEstCreee(): void
|
|
946
|
+
{
|
|
947
|
+
if ($this->exception) {
|
|
948
|
+
throw $this->exception;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (!$this->reservationId) {
|
|
952
|
+
throw new \RuntimeException('No reservation created');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* @Then le montant total est de :montant
|
|
958
|
+
*/
|
|
959
|
+
public function leMontantTotalEstDe(string $montant): void
|
|
960
|
+
{
|
|
961
|
+
$repository = $this->kernel->getContainer()->get(ReservationRepositoryInterface::class);
|
|
962
|
+
$reservation = $repository->findById(ReservationId::fromString($this->reservationId));
|
|
963
|
+
|
|
964
|
+
$expectedAmount = (float) str_replace('€', '', $montant);
|
|
965
|
+
$actualAmount = $reservation->getMontantTotal()->getAmountEuros();
|
|
966
|
+
|
|
967
|
+
if ($actualAmount !== $expectedAmount) {
|
|
968
|
+
throw new \RuntimeException(
|
|
969
|
+
sprintf('Expected %s€, got %s€', $expectedAmount, $actualAmount)
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### Behat ausführen
|
|
977
|
+
|
|
978
|
+
```bash
|
|
979
|
+
# Alle Szenarien
|
|
980
|
+
make behat
|
|
981
|
+
|
|
982
|
+
# Spezifisches Szenario
|
|
983
|
+
make behat ARGS="--name='Créer une réservation avec 2 participants'"
|
|
984
|
+
|
|
985
|
+
# Erwartete Ausgabe:
|
|
986
|
+
# Feature: Créer une réservation
|
|
987
|
+
# Scenario: Créer une réservation avec 2 participants
|
|
988
|
+
# ✓ When je crée une réservation...
|
|
989
|
+
# ✓ Then la réservation est créée
|
|
990
|
+
# ✓ And le montant total est de "1000€"
|
|
991
|
+
#
|
|
992
|
+
# 1 scenario (1 passed)
|
|
993
|
+
# 3 steps (3 passed)
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
---
|
|
997
|
+
|
|
998
|
+
## Mutations-Testing mit Infection
|
|
999
|
+
|
|
1000
|
+
### Konfiguration infection.json5
|
|
1001
|
+
|
|
1002
|
+
```json5
|
|
1003
|
+
{
|
|
1004
|
+
"$schema": "vendor/infection/infection/resources/schema.json",
|
|
1005
|
+
"source": {
|
|
1006
|
+
"directories": ["src"]
|
|
1007
|
+
},
|
|
1008
|
+
"logs": {
|
|
1009
|
+
"text": "var/infection/infection.log",
|
|
1010
|
+
"html": "var/infection/index.html"
|
|
1011
|
+
},
|
|
1012
|
+
"mutators": {
|
|
1013
|
+
"@default": true
|
|
1014
|
+
},
|
|
1015
|
+
"minMsi": 80,
|
|
1016
|
+
"minCoveredMsi": 90
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### Infection ausführen
|
|
1021
|
+
|
|
1022
|
+
```bash
|
|
1023
|
+
# Mutations-Testing
|
|
1024
|
+
make infection
|
|
1025
|
+
|
|
1026
|
+
# Erwartete Ausgabe:
|
|
1027
|
+
# Infection - PHP Mutation Testing Framework
|
|
1028
|
+
#
|
|
1029
|
+
# Running mutation tests...
|
|
1030
|
+
#
|
|
1031
|
+
# Mutations: 150
|
|
1032
|
+
# Killed: 120 (80%)
|
|
1033
|
+
# Escaped: 20 (13.3%)
|
|
1034
|
+
# Errors: 5 (3.3%)
|
|
1035
|
+
# Timed Out: 5 (3.3%)
|
|
1036
|
+
#
|
|
1037
|
+
# Mutation Score Indicator (MSI): 80%
|
|
1038
|
+
# Covered Code MSI: 92%
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### Beispiele für Mutationen
|
|
1042
|
+
|
|
1043
|
+
```php
|
|
1044
|
+
// Originalcode
|
|
1045
|
+
if ($amount > 0) {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Mutation 1: Operator verändert
|
|
1050
|
+
if ($amount >= 0) { // ❌ Muss durch einen Test getötet werden
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Mutation 2: Bedingung umgekehrt
|
|
1055
|
+
if ($amount < 0) { // ❌ Muss durch einen Test getötet werden
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Mutation 3: Rückgabewert verändert
|
|
1060
|
+
if ($amount > 0) {
|
|
1061
|
+
return false; // ❌ Muss durch einen Test getötet werden
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Wenn eine Mutation überlebt = fehlender oder schwacher Test!**
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## Validierungs-Checkliste
|
|
1070
|
+
|
|
1071
|
+
### Vor jedem Commit
|
|
1072
|
+
|
|
1073
|
+
- [ ] **TDD:** Tests VOR der Implementierung geschrieben
|
|
1074
|
+
- [ ] **RED:** Test schlägt anfangs fehl
|
|
1075
|
+
- [ ] **GREEN:** Minimale Implementierung lässt den Test bestehen
|
|
1076
|
+
- [ ] **REFACTOR:** Code verbessert, Tests bleiben grün
|
|
1077
|
+
- [ ] **Coverage:** `make test-coverage` → mindestens 80 %
|
|
1078
|
+
- [ ] **Mutation:** `make infection` → MSI mindestens 80 %
|
|
1079
|
+
- [ ] **BDD:** Behat-Szenarien für fachliche Funktionalitäten
|
|
1080
|
+
- [ ] **Fast:** Unit-Tests < 100 ms pro Test
|
|
1081
|
+
- [ ] **Isolated:** Tests unabhängig (zufällige Reihenfolge OK)
|
|
1082
|
+
|
|
1083
|
+
### Zielmetriken
|
|
1084
|
+
|
|
1085
|
+
| Metrik | Ziel | Minimum |
|
|
1086
|
+
|--------|------|---------|
|
|
1087
|
+
| Code-Coverage | 85 % | 80 % |
|
|
1088
|
+
| Mutation Score (MSI) | 85 % | 80 % |
|
|
1089
|
+
| Covered Code MSI | 95 % | 90 % |
|
|
1090
|
+
| Unit-Tests | < 100 ms | < 200 ms |
|
|
1091
|
+
| Integrationstests | < 1 s | < 2 s |
|
|
1092
|
+
| Funktionale Tests | < 5 s | < 10 s |
|
|
1093
|
+
|
|
1094
|
+
### Validierungsbefehle
|
|
1095
|
+
|
|
1096
|
+
```bash
|
|
1097
|
+
# Vollständige Pipeline
|
|
1098
|
+
make test # Alle Tests
|
|
1099
|
+
make test-coverage # Mit Coverage
|
|
1100
|
+
make infection # Mutations-Testing
|
|
1101
|
+
make behat # BDD-Tests
|
|
1102
|
+
|
|
1103
|
+
# CI-Validierung
|
|
1104
|
+
make ci
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
1109
|
+
## Ressourcen
|
|
1110
|
+
|
|
1111
|
+
- **Buch:** *Test-Driven Development by Example* - Kent Beck
|
|
1112
|
+
- **Buch:** *Growing Object-Oriented Software, Guided by Tests* - Steve Freeman
|
|
1113
|
+
- **PHPUnit:** [Dokumentation](https://phpunit.de/documentation.html)
|
|
1114
|
+
- **Behat:** [Dokumentation](https://docs.behat.org/)
|
|
1115
|
+
- **Infection:** [Dokumentation](https://infection.github.io/)
|
|
447
1116
|
|
|
448
1117
|
---
|
|
449
1118
|
|