Correction du brief L'Arene. Trois ecoles de combat (Guerrier, Mage, Archer) creent des combattants dont chaque statistique est choisie dans les bornes de son ecole, puis on les fait s'affronter.
Chaque etape du brief a sa branche :
| Branche | Contenu |
|---|---|
main |
= correction/etape-1 |
correction/etape-1 |
hierarchie, validation des bornes, getInformation(), tests de construction |
correction/etape-2 |
specialAbility(), feat(), modifier(), power() |
correction/etape-3 |
Ranking, FightManager.fight1v1 |
correction/etape-4 |
Element et le cycle des affinites |
correction/etape-5 |
fightContest, version boucles et version streams |
src/
Main.java cree un combattant de chaque ecole et les affiche
main/
Fighter.java classe mere abstraite : nom, hp, strength, speed
Warrior.java hp 96-106, strength 20-24, speed 8-12, rage 12-18
Mage.java hp 88-98, strength 14-18, speed 18-22, mana 32-48
Archer.java hp 90-100, strength 17-21, speed 21-25, precision 20-30
test/
WarriorTest.java construction valide + refus de chaque borne
MageTest.java
ArcherTest.java
Ou vit quoi, et pourquoi :
- Dans
Fighter: ce qui est identique pour les trois ecoles — les trois statistiques de base et leurs getters, le helpervalidateAttribute, etgetInformation(). - Dans chaque ecole : ses bornes (constantes
HP_MIN,HP_MAX...), son attribut propre avec son getter, et la redefinition degetInformation()qui ajoute cet attribut a la description commune.
Les trois combattants du brief, utilises tels quels dans les tests :
| Combattant | Statistiques |
|---|---|
| Warrior « Bjorn » | hp 100, strength 22, speed 10, rage 15 |
| Mage « Lyra » | hp 90, strength 16, speed 20, mana 40 |
| Archer « Sylve » | hp 95, strength 19, speed 23, precision 25 |
Ce sont deux moments distincts, et confondre les deux fait perdre beaucoup de temps en debogage.
Une erreur de compilation empeche le programme d'exister. Le compilateur refuse de traduire votre code, il n'y a donc rien a lancer : pas une seule ligne ne s'execute, meme celles qui etaient parfaitement correctes. Un point-virgule oublie, une methode appelee qui n'existe pas, un type qui ne correspond pas : le projet ne compile pas, point.
Une exception, elle, arrive a l'execution. Le code etait valide, il a compile, le programme
a demarre, et il s'interrompt en cours de route. C'est exactement le cas du Warrior cree avec
hp = 500 : ce code est parfaitement correct du point de vue du compilateur — 500 est bien un
int, le constructeur existe et attend bien un int. C'est seulement pendant l'execution, au
moment ou la valeur est reellement testee contre les bornes, que le throw part.
Comment les distinguer en pratique : une erreur de compilation est signalee avant que vous n'ayez lance quoi que ce soit (souligne en rouge dans IntelliJ, le bouton Run echoue tout de suite). Une exception s'affiche pendant l'execution, sous forme de trace de pile dans la console, apres que les lignes precedentes se sont deja executees et ont deja affiche leur resultat.
assertThrows fonctionne a l'envers de toutes les autres assertions, et c'est le piege
numero un de cette etape.
assertEquals(100, bjorn.getHp()): vert si le code donne le bon resultat.assertThrows(IllegalArgumentException.class, () -> new Warrior(...)): vert si le code echoue.
Autrement dit : le test est OK si le code est KO, et KO si le code est OK. Un test
hpTooLowThrows qui passe au rouge ne veut pas dire « le constructeur a plante », il veut dire
l'inverse : le constructeur a accepte une valeur qu'il aurait du refuser.
assertThrows verifie seulement qu'une exception du bon type a ete levee. Il ne dit pas
laquelle. Avec quatre attributs a valider et huit tests de bornes par ecole, c'est vite
ingerable : un test cense verifier que hp est refuse peut passer au vert parce que c'est la
validation de speed qui a leve (un erreur de copier coller est vite arrivée).
Le test est vert, il ne teste pas ce qu'on croit, et il resterait vert meme si la
validation de hp disparaissait completement. D'ou l'assertion supplementaire sur le message :
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> new Warrior("Bjorn", 95, 22, 10, 15));
assertTrue(error.getMessage().contains("hp"));On ne verifie plus seulement qu'il y a eu une erreur, mais qu'on est bien tombe sur la
bonne. C'est aussi ce qui justifie de construire le message a partir du nom de l'attribut
dans validateAttribute : sans message parlant, il n'y a rien a assertionner.
Les attributs d'un objet doivent etre prives : private final int hp;. C'est
l'encapsulation — personne ne peut les lire ni les modifier directement depuis l'exterieur de
la classe.
La consequence est tres concrete : sans getter, votre objet est une boite noire. Les tests ne peuvent rien verifier et on ne peut pas réccupèrer les valeur ailleurs que dans leurs propre classe.
C'est pour ca que constructsWithValidValues a besoin de getName(), getHp(), getStrength(), getSpeed() et du getter de l'attribut propre.
Ou les placer suit la meme logique que le reste :
- dans
Fighter, les getters des attributs communs (getName,getHp,getStrength,getSpeed) ; - dans chaque ecole, le getter de son attribut propre (
getRage,getMana,getPrecision), parce que cet attribut n'existe que la.
L'appel au constructeur de la classe mere passe toujours en premier, c'est impose par Java. Les champs sont donc bien affectes, puis la validation leve.
L'objet a moitie initialise a bien existe une fraction de seconde. Mais comme le constructeur ne se termine jamais, aucune reference n'est rendue a l'appelant : la variable n'est jamais affectee, et le ramasse-miettes recupere l'objet. Du point de vue du programme, ce combattant invalide n'a jamais existe.
C'est la question de reflexion de cette etape, detaillee dans REFLEXION.md.