
Cet article est le quatrième (et normalement dernier) de la série concernant le kata Digging Estimator, un kata créé par mes soins à destination des élèves d’un module de tests
Après avoir utilisé l’approval testing pour déclencher un refactoring, il est temps d’implémenter une nouvelle fonctionnalité à l’aide du Test Driven Development.
Les autres articles :
- Part 1 – Le problème
- Part 2 – Approval testing
- Part 3 – Refactoring
- Part 4 – Ajout de fonctionnalité (ce post)
La fonctionnalité
La V.I.N. a récemment mis à disposition un nouveau service pour savoir s'il y a des gobelins dans la zone. L'URL : `dtp://research.vin.co/are-there-goblins/{location}` (DTP = Dwarfish Transfert Protocol) Il faut donc que notre estimateur prenne en entrée la région dans laquelle on veut creuser (sous forme de chaine de caractères). Le service répond `true` ou `false`. S'il y a des gobelins, alors : * il faut __ajouter au chantier 2 protecteurs (`protectors`) à chaque équipe__ (jour et nuit). * de nuit, __ces protecteurs ont chacun besoin d'un éclaireur__ (`lighter`) pour les éclairer. On se bat nettement moins bien dans le noir * évidemment, les protecteurs consomment de la bière, donc __ils impactent le nombre de taverniers__ (`innkeepers`) nécessaires * ils génèrent donc de la vaisselle, donc __ils augmentent potentiellement le nombre de laveurs de vaisselle__ (`washers`) * en revanche __pas besoin d'ajouter des gardes pour eux__. Ils sont armés, donc ça ne ferait qu'augmenter le nombre de soigneurs nécessaires
La démarche
Nous allons utiliser le Test Driven Development pour coder la fonctionnalité. Si jamais on s’aperçoit à un moment qu’il faut trop de changements dans le code, et qu’un complément de refacto est nécessaire, on rangera le test dans un coin le temps de refactorer.
C’est parti.
Un début en fanfare
Tous les tests présents assument qu’il n’y avait pas de gobelin dans la zone. Il ne me reste plus qu’à créer un nouveau bloc avec des gobelins. Sauf que pour ça, il me faut un GoblinInformationService
.
describe("digging estimator", () => { let estimator: DiggingEstimator; beforeEach(() => { estimator = new DiggingEstimator(new FakeRockInformationService(), new FakeGoblinInformationService()); });
On va le définir :
class FakeGoblinInformationService { private areThereGoblins = false; checkForGoblins(area: string) { return this.areThereGoblins } setGoblinPresence(areThereGoblins: boolean) { this.areThereGoblins = areThereGoblins; } }
Je suis allé un peu vite ici. J’ai besoin d’une méthode pour setter les gobelins dynamiquement, vu que je crée mon DiggingEstimator
une seule fois. Je dois du coup l’extraire :
describe("digging estimator", () => { let estimator: DiggingEstimator; let fakeGoblinInformationService: FakeGoblinInformationService beforeEach(() => { fakeGoblinInformationService = new FakeGoblinInformationService(); estimator = new DiggingEstimator(new FakeRockInformationService(), fakeGoblinInformationService); });
Mon IDE est mécontent. Il me dit que DiggingEstimator
ne prend qu’un paramètre. Il est temps d’extraire une interface, et de l’ajouter (de façon optionnelle, pour ne pas casser la rétro-compat).
export interface GoblinInformationService { checkForGoblins(area: string): boolean; }
export class DiggingEstimator { private readonly rockInformation: RockInformationInterface; private readonly goblinInformation: GoblinInformationService; constructor(rockInformation?: RockInformationInterface, goblinInformation?: GoblinInformationService) { this.rockInformation = rockInformation || new VinRockInformationService() this.goblinInformation = goblinInformation; }
IDE toujours pas content. Ligne 7, goblinInformation
peut être null. Normal. Il est temps de créer la classe concrète de production. Je reviendrai plus tard à comment je la teste.
export class VinGoblinInformationService implements GoblinInformationService { checkForGoblins(area: string): boolean { return false; } }
IDE satisfait, les tests tournent à nouveau. Je peux écrire le test que je voulais.
describe("when there are goblins in the area", function() { beforeEach(() => { fakeGoblinInformationService.setGoblinPresence(true); }) it('should add 2 protectors to day team', () => { estimator.tunnel(ONE_DWARF_DIG_PER_ROTATION, 1, "granite", "Moria"); }) });
IDE rouge. Paramètre Moria
inconnu. L’heure d’ajouter du code de prod.
tunnel(length: number, days: number, rockType: string, location = ""): TeamComposition {
IDE vert. Tests verts. Pas de refacto en vue. Je continue mon test :
it('should add 2 protectors to day team', () => { const result = estimator.tunnel(ONE_DWARF_DIG_PER_ROTATION, 1, "granite", "Moria"); expect(result.dayTeam.protectors).toBe(2); })
IDE rouge. protectors
inconnu. Je le définis dans Team
directement. IDE vert, tests… rouges (forcément). L’heure de coder un peu côté Prod. Je vais partir du plus haut niveau et laisser mon IDE me guider :
tunnel(length: number, days: number, rockType: string, location = ""): TeamComposition { this.checkInputs(length, days); const areThereGoblins = location ? this.goblinInformation.checkForGoblins(location) : false const diggingInfo = new DiggingInfo(length, days, this.rockInformation.get(rockType), areThereGoblins)
areThereGoblins
non défini dans DiggingInfo, je le crée :
export class DiggingInfo { readonly distanceToDigPerDay; readonly maxDiggingDistancePerRotation; constructor(readonly lengthToDig: number, readonly daysAvailable: number, readonly digPerDwarfPerRotation: number[], readonly areThereGoblins: boolean) { this.distanceToDigPerDay = Math.floor(lengthToDig / daysAvailable); this.maxDiggingDistancePerRotation = digPerDwarfPerRotation.at(-1) || 0; } }
Voyons voir la suite de tunnel
:
this.checkTunnelCanBeDugInRequestedTime(diggingInfo); return TeamComposition.fromDiggingInfo(diggingInfo);
On descend dans fromDiggingInfo
, qui nous fait descendre dans DayTeam
:
export class DayTeam extends Team { static fromDiggingInfo(diggingInfo: DiggingInfo): DayTeam { const team = new DayTeam(); team.miners = team.computeMiners(diggingInfo.digPerDwarfPerRotation, diggingInfo.distanceToDigPerDay); team.protectors = team.computeProtectors(diggingInfo.areThereGoblins); team.computeOtherDwarves(); return team; }
On va définir la méthode computeProtectors
directement dans Team
(il est clair que NightTeam va avoir besoin de la même) :
const NEEDED_PROTECTORS = 2; export class Team { // ... protected computeProtectors(areThereGoblins: boolean): number { return areThereGoblins ? NEEDED_PROTECTORS : 0; }
Je lance les tests. Ils sont verts.
Petit commit (que je n’enverrai pas sur le dépôt central tout de suite !) : feat: add protectors to dayteam
Pas de refacto en vue, le code s’est inséré tout seul.
Étendre le test
Le nombre de protecteurs impacte forcément le total de nains, ce que je n’ai pas vérifié. J’ajoute un test, en retouchant légèrement la structure de test au passage :
it('should add 2 protectors to team composition total as well', () => { const result = estimator.tunnel(ONE_DWARF_DIG_PER_ROTATION, 1, "granite", "Moria"); expect(result.total).toBe(15); })
Test rouge. Il a obtenu 9
. C’est normal, j’ai mis 15 au hasard, je ne me souvenais plus de combien de nains j’attendais sans les protecteurs. Du coup :
expect(result.total).toBe(11);
C’est toujours rouge, mais cette fois pour la bonne raison. Je peux coder dans Team.total
:
get total(): number { return this.miners + this.washers + this.healers + this.smithies + this.innKeepers + this.guards + this.guardManagers + this.lighters + this.protectors; }
Tests OK. Pas de refacto en vue. Je continue. Dans certains cas, ajouter des protecteurs change le nombre d’aubergistes nécessaires. C’est le cas pour 1 mineur, parce qu’1 mineur + 1 soigneur + 2 forgerons demandent 4 aubergistes, donc 2 protecteurs de plus impliquent 4 aubergistes supplémentaires.
it('should add innkeepers for protectors if needed protectors to team composition total as well', () => { const result = estimator.tunnel(ONE_DWARF_DIG_PER_ROTATION, 1, "granite", "Moria"); expect(result.dayTeam.innKeepers).toBe(8); })
Je vais avoir un problème. Parce qu’en fait, si j’ajoute 4 taverniers, mon total va déconner, donc je vais casser un autre test. Je n’ai pas vraiment le choix :
- Si je commente ce test et que je corrige le total, le test commenté sera vert quand je le décommenterai
- Si je garde ce test et que je l’implémente, je casserai un autre test
Dans les deux cas, en théorie, je n’ai pas le droit. En pratique, je me l’octroie : l’incrément est suffisamment petit pour que je ne me perde pas.
Donc, le code :
export class DayTeam extends Team { // ... computeOtherDwarves() { if (this.miners > 0) { this.healers = 1; this.smithies = 2; this.innKeepers = Math.ceil((this.miners + this.healers + this.smithies + this.protectors) / 4) * 4; this.washers = Math.ceil((this.miners + this.healers + this.smithies + this.innKeepers) / 10); } }
L’autre test a cassé, je le corrige. Rha, non, il affiche que le résultat était 16. J’ai ajouté un laveur de vaisselle parce que je suis passé au-dessus des dix nains. Enfin c’est mon hypothèse. Je vais la vérifier.
it('should add washers if innkeepers or protectors increase requests one', () => { const result = estimator.tunnel(ONE_DWARF_DIG_PER_ROTATION, 1, "granite", "Moria"); expect(result.dayTeam.washers).toBe(2); })
C’est bien ça, le test est vert avec 2 washers
, alors qu’il n’en fallait qu’un au début. Je peux corriger mon total en toute tranquillité :
expect(result.total).toBe(16);
Petit commit, parce qu’on a fait pas mal de chose: feat: add impact of protectors on innkeepers for day team
Je n’ai toujours pas de cas où c’est le nombre de protecteurs qui fait déborder les washers. Je regarde mon tableau… et m’aperçois que sur l’équipe de jour, ça ne peut pas se produire (sans protecteurs, j’ai 8, 13 et 14 nains suivant la distance). Est-ce que j’ajoute le code quand même ?
Oui ! Tant pis pour l’absence de tests, si jamais je ne le mets pas, à chaque fois que je vais regarder le code je vais me demander si on n’a pas oublié une règle.
En fait, je me dis que si je le mets en évidence pour la nuit, un refacto consistant à créer une méthode computeWashers
dans Team
serait la bienvenue. Parce que la règle est en fait la même le jour et la nuit.
Tiens, je vais le faire de suite en fait. Rien ne m’empêche de continuer à améliorer mon code en phase de refacto, et de toutes façons je viens de finir l’équipe de jour, donc je ne suis pas au milieu de quelque chose.
export class Team { //... protected computeGuardManagers() { return Math.ceil((this.guards) / 3); } protected computeGuards() { return Math.ceil((this.healers + this.miners + this.smithies + this.lighters + this.washers) / 3); } protected computeWashers() { return Math.ceil((this.miners + this.healers + this.smithies + this.innKeepers + this.lighters + this.guards + this.guardManagers) / 10); } protected computeInnKeepers() { return Math.ceil((this.miners + this.healers + this.smithies + this.lighters) / 4) * 4; } }
C’est mon IDE qui a tout fait (Ctrl + Alt+ M
pour extraire les méthodes dans NightTeam
, qui a la version complète, puis Ctrl + Alt + Shift + T -> Pull Members Up
pour les envoyer dans Team
). Il me reste quand même à modifier DayTeam
:
computeOtherDwarves() { if (this.miners > 0) { this.healers = 1; this.smithies = 2; this.innKeepers = this.computeInnKeepers(); this.washers = this.computeWashers(); } }
Et quand j’écris ça, je me dis que je suis peut-être passé à côté d’un bien joli pattern Template
. Mais là, c’est pas le moment, ça ferait trop de changements.
Mes tests sont… ah. Rouges. Tous les tests ajoutant des protecteurs ont planté. Évidemment, quelle andouille, j’ai viré la partie + this.protectors
de la DayTeam
. Je corrige Team
:
export class Team { protected computeWashers() { return Math.ceil((this.miners + this.healers + this.smithies + this.innKeepers + this.lighters + this.guards + this.guardManagers + this.protectors) / 10); } protected computeInnKeepers() { return Math.ceil((this.miners + this.healers + this.smithies + this.lighters + this.protectors) / 4) * 4; }
Ok, c’est bon. Petit commit : refactor: move some calculation to Team
Le cas de la nuit
Il ne me reste plus qu’à écrire les tests pour la nuit (en sachant que le jour se retrouve impacté). On va commencer simple :
it("should add two protectors to night Team", function() { const result = estimator.tunnel(THREE_DWARVES_DIG_PER_ROTATION + ONE_DWARF_DIG_PER_ROTATION, 1, GRANITE, "Moria"); expect(result.nightTeam.protectors).toBe(2); });
Rouge. Parfait. Le code de NightTeam
devient :
static fromDiggingInfo(diggingInfo: DiggingInfo): NightTeam { const team = new NightTeam(); team.miners = team.computeMiners(diggingInfo.digPerDwarfPerRotation, diggingInfo.distanceToDigPerDay - diggingInfo.maxDiggingDistancePerRotation); team.protectors = team.computeProtectors(diggingInfo.areThereGoblins); team.computeOtherDwarves(); return team; }
J’ajoute furieusement sur ma liste que le pattern Template se précise. Rouge. J’ai un bug, certains tests de jour ne fonctionnent plus. Il ne me faut pas longtemps pour réaliser que j’ajoute des protecteurs même s’il n’y a pas de mineurs. J’ajuste computeProtectors
:
protected computeProtectors(areThereGoblins: boolean): number { return areThereGoblins && this.miners > 0 ? NEEDED_PROTECTORS : 0; }
Ok, Vert. J’avais un petit problème d’imbrication de mes bloc describe
auquel je n’avais pas fait attention, mais c’est réglé.
Je dois voir l’impact de mes protector
sur les innKeepers
. Une analyse du tableau que j’ai construit durant l’approval testing me montre que le nombre de innKeepers
augmentera dans le cas où j’ai 2 miners
la nuit :
it('should impact the number of innkeepers for the night team', function() { const result = estimator.tunnel(Math.floor(THREE_DWARVES_DIG_PER_ROTATION + TWO_DWARVES_DIG_PER_ROTATION), 1, GRANITE, "Moria"); expect(result.nightTeam.innKeepers).toBe(12); });
Le test est vert d’entrée de jeu. C’est normal. Ce test n’est pas nécessaire d’un point de vue purement TDD, mais je vais le garder parce qu’il illustre une règle métier. C’est un tradeoff : j’accepte un test de plus à maintenir en échange d’un peu plus de clarté si je manipule le code et qu’il explose.
Voyons voir pour les washers
. Avec 2 miners
, de nuit, j’ai 29 nains hors washers. Donc si j’ajoute mes 2 protectors
, je devrais avoir 31 nains, donc 4 washers
:
it('should impact the number of washers for the night team', function() { const result = estimator.tunnel(THREE_DWARVES_DIG_PER_ROTATION + THREE_DWARVES_DIG_PER_ROTATION, 1, GRANITE, "Moria"); expect(result.nightTeam.washers).toBe(4); });
C’est vert. Normal, je l’ai implémenté directement dans Team
(ou plutôt je l’ai refactoré vers Team
). Il ne me reste plus qu’à vérifier le total. Avec 3 miners
de nuit, on n’impacte pas le nombre d’innKeepers, donc pas le nombre de gardes. Du coup, on a juste 3 nains de plus que sans protector
:
it("should impact the total for the night team", function() { const result = estimator.tunnel(THREE_DWARVES_DIG_PER_ROTATION + THREE_DWARVES_DIG_PER_ROTATION, 1, GRANITE, "Moria"); const expectedDayTeamSize = 18; const expectedNightTeamSize = 35; expect(result.total).toBe(expectedDayTeamSize + expectedNightTeamSize); });
On a fini. Ça n’aura pas pris longtemps, le changement était somme toute facile à intégrer. Commit : feat: implement protectors for night team
.
Chef, il fait tout noir par ici !
J’ai oublié une ligne dans les specs :
de nuit, ces protecteurs ont chacun besoin d’un éclaireur (
lighter
) pour les éclairer. On se bat nettement moins bien dans le noir
Bon, j’ajoute le test :
it("should add two lighters for the two protectors", function() { const result = estimator.tunnel(THREE_DWARVES_DIG_PER_ROTATION + ONE_DWARF_DIG_PER_ROTATION, 1, GRANITE, "Moria"); const lightersNeededForMiners = 1; const lightersNeededForRestOfCamp = 1; const lightersNeededForProtectors = 2; expect(result.nightTeam.lighters).toBe(lightersNeededForMiners + lightersNeededForProtectors + lightersNeededForRestOfCamp); });
Rouge. La bonne nouvelle, c’est que l’implémentation dans NightTeam
est triviale :
computeOtherDwarves() { if (this.miners > 0) { this.healers = 1; this.smithies = 2; this.lighters = this.miners + this.protectors + 1;
Le test est vert, mais j’en ai un qui est rouge. Normal, ajouter des lighters
peut changer le nombre de innKeepers
, de guards
, de guardManagers
et de washers
.
Posons les données d’un tunnel nécessitant 3 miners
le jour et 3 miners
la nuit.
- Le jour :
- 3
miners
- 1
healer
- 2
smithies
- 2
protectors
- 8
innKeepers
(puisque (3 +1 + 2 + 2 / 4) * 4) - 0
guards
, 0lighters
, 0guardManagers
- ça fait donc 16 nains => 2
washers
- Total : 18 nains
- 3
- La nuit
- 3
miners
- 1
healer
- 2
smithies
- 2
protectors
- 6
lighters
( 3 pour lesminers
, 2 pour lesprotectors
, 1 pour le reste du camp) - 16
innKeepers
((3 + 1 +2+2+6) = 14 / 4 = 4 arrondi au supérieur, * 4 = 16) - Ça fait 30 nains sans les gardes, donc mettons tout de suite 4
washers
- 6
guards
(3 + 1 + 2 + 6 + 4 = 16 / 3 = 6 arrondi au supérieur) - 2
guardManagers
(6 / 3) - Ça rajoute 8 nains aux 30 précédents, donc 4
washers
c’était très bien - Total : 42 nains
- 3
- Soit au grand total : 60 nains
J’ajuste mon test. Je vais le rendre plus robuste pour être tranquille :
it("should impact the total for the night team", function() { const result = estimator.tunnel(THREE_DWARVES_DIG_PER_ROTATION + THREE_DWARVES_DIG_PER_ROTATION, 1, GRANITE, "Moria"); const expectedDayTeam = createTeam(3,1,2,0,8,0,0,2,2); const expectedNightTeam = createTeam(3,1,2,6,16,6,2,4,2); const expectedDayTeamSize = 18; const expectedNightTeamSize = 42; expect(result).toEqual(new TeamComposition(expectedDayTeam, expectedNightTeam)) expect(result.total).toBe(expectedDayTeamSize + expectedNightTeamSize); });
J’ai dû ajouter protectors
à createTeam
, mais ça reste simple. Tout est vert, commit : feat: add lighters for protectors
Le pattern « Template » ?
J’ai détecté la possibilité d’utiliser un pattern Template
tout à l’heure, mais je n’y ai pas réfléchi.
L’idée d’un template, c’est que Team
ait une méthode computeTeam
, qui appelle plein de sous-fonctions pour calculer chaque poste. Les classes DayTeam
et NightTeam
auront la possibilité de redéfinir ces méthodes suivant leurs besoins. Sauf que le pattern template marche bien si j’ai une implémentation par défaut pour chaque méthode, ou que je peux rendre ces méthodes abstraites. Or, quelle implémentation par défaut prendre pour computeGuards
? Celle de jour ou de nuit ? Et je ne peux pas rendre Team
abstraite, je ne sais pas comment les gens s’en servent. Il faudrait que je vérifie avant.
J’abandonne l’idée pour le moment. C’est l’heure de vérifier si on a bien travaillé.
Couverture de test
Je suis curieux de vérifier mes couvertures de test et de mutation, voir si j’ai oublié quelque chose. On essaie :

digging-estimator
, c’est bon signe. J’en profite pour remarquer que j’ai une typo dans mes dossiers, j’ai un fichier qui a atterri dans externa
au lieu de external
. Je corrige.Commit: refactor: minor adjustment
Je lance les tests de mutations. Première constatation : j’ai moins de mutants que tout à l’heure, alors que j’ai ajouté du code. Mon refacto y est pour quelque chose.

Woohoo, 96% ! Il me reste cependant un problème que j’avais listé au tout début de l’implémentation : je n’ai pas de test pour les services externes. En fait, je n’ai même pas d’implémentation…
Tester les services externes ?
Je pourrais implémenter la méthode appelant la V.I.N. pour aller voir s’il y a des gobelins directement :
export class VinGoblinInformationService implements GoblinInformationService { checkForGoblins(area: string): boolean { const url = `dtp://research.vin.co/are-there-goblins/${area}`; console.log(`Tried to fetch ${url}`); throw new Error('Does not work in test mode'); } }
Et j’aurais une couverture presque identique à celle du VinRockInformationService
. Le problème c’est que je ne vérifie pas l’URL d’appel dans mes tests. En fait, le code me fait maintenant apparaître qu’il y a un service de communication qui envoie des informations au V.I.N.
Pour l’instant, je ne contacte que le V.I.N. Donc on doit pouvoir se dire qu’il faut juste un canal de communication. Quelque chose qui ressemblerait à :
export class VinCommunicationService { get<T>(url: string): T { console.log(`Tried to fetch ${url}`); throw new Error('Does not work in test mode'); } }
La méthode est paramétrée par un template pour aider à l’auto-complétion, rien d’autre. Du coup on pourrait s’en servir comme cela :
export class VinGoblinInformationService implements GoblinInformationService { private communicationService = new VinCommunicationService(); checkForGoblins(area: string): boolean { const url = `dtp://research.vin.co/are-there-goblins/${area}`; return this.communicationService.get<boolean>(url); } }
Si j’y avais réfléchi plus tôt, c’est là que j’aurais placé la frontière de mon module. Maintenant, j’ai deux choix :
- Je peux tester les services d’information individuellement. Très facile. Moins intégré.
- Je peux tester les services avec le
DiggingEstimator
, et refacto tous mes tests. Plus douloureux. Plus intégré.
Je vais prendre la première solution. D’une parce que les risques d’une cassure à l’intégration sont minimales et ne justifient pas un effort de refacto complet. De deux parce cette série de post est beaucoup trop longue ! On y va.
Je vais faire des plus grands pas dans mon cycle de dév, quitte à me planter. Je commence par tester le VinGoblinInformationService
:
export interface Communication { get<T>(url: string): T; } class FakeCommunicationService implements Communication { calledUrl = ""; get<T>(url: string) { this.calledUrl = url; return true as T; } } describe("VinGoblinInformationService", function() { it("should call the communication service with correct url", function() { const communicationService = new FakeCommunicationService(); const service = new VinGoblinInformationService(communicationService); service.checkForGoblins("Moria"); expect(communicationService.calledUrl).toBe('dtp://research.vin.co/are-there-goblins/Moria'); }); });
Rouge. Le code du VinGoblinInformationService
devient :
import { GoblinInformationService } from "../goblin-information-service.interface"; import { Communication } from "./communication.interface"; export class VinGoblinInformationService implements GoblinInformationService { constructor(private readonly communicationService: Communication) { this.communicationService = communicationService; } checkForGoblins(area: string): boolean { const url = `dtp://research.vin.co/are-there-goblins/${area}`; return this.communicationService.get<boolean>(url); } }
Vert. Un petit test pour le comportement nominal est c’est bon :
it("should throw an error when called with default communication service", function() { const service = new VinGoblinInformationService(); expect(() => service.checkForGoblins("Moria")).toThrow(); });
Et le code :
export class VinGoblinInformationService implements GoblinInformationService { private readonly communicationService; constructor(communicationService?: Communication) { this.communicationService = communicationService || new VinCommunicationService(); }
Faisons pareil pour le
RockInformationService
:
import { FakeCommunicationService } from "./test/fake-communication.service"; import { VinRockInformationService } from "./vin-rock-information.service"; describe("VinRockInformationService", function() { it("should call the communication service with correct url", function() { const communicationService = new FakeCommunicationService(); const service = new VinRockInformationService(communicationService); service.get("Granite"); expect(communicationService.calledUrl).toBe('dtp://research.vin.co/digging-rate/granite'); }); it("should throw an error when called with default communication service", function() { const service = new VinRockInformationService(); expect(() => service.get("granite")).toThrow(); }); });
J’ai tout écrit en une fois, et dû extraire le FakeCommunicationService dans son propre fichier, mais le changement est maîtrisé, je me permets donc de faire des itérations un peu plus grandes.
import { RockInformationInterface } from "../rock-information.interface"; import { Communication } from "./communication.interface"; import { VinCommunicationService } from "./vin-communication.service"; export class VinRockInformationService implements RockInformationInterface { private readonly communicationService: Communication; constructor(communicationService?: Communication) { this.communicationService = communicationService || new VinCommunicationService(); } get(rockType: string): number[] { // For example, for granite it returns [0, 3, 5.5, 7] // if you put 0 dwarf, you dig 0m/d/team // if you put 1 dwarf, you dig 3m/d/team // 2 dwarves = 5.5m/d/team // so a day team on 2 miners and a night team of 1 miner dig 8.5m/d const url = `dtp://research.vin.co/digging-rate/${rockType}`; return this.communicationService.get<number[]>(url); } }
Tout est vert, le coverage est à 100%. Commit final : Petit commit : refactor: extract communication service for GoblinService
Conclusion
La technique utilisée par ce kata est un classique. Rattrapper un code inconnu (et mal foutu) par l’approval testing, le refactorer jusqu’à obtenir une version suffisamment agréable pour ajouter de nouvelles fonctionnalités.
J’ai cependant tenté de vous livrer le processus tel que je l’utilise, après dix ans à développer à l’aide du TDD, et quelques années à pousser le refactoring de plus en plus loin. Comme vous le voyez, mon process est loin d’être parfait, mais les tests et les outils m’offrent la sécurité et le droit de me tromper.
Mais on peut toujours refactorer toujours plus. Il est important de savoir s’arrêter quand le refactoring est suffisant, tout en gardant à l’esprit qu’on peut toujours refactorer au besoin pendant qu’on développe la fonctionnalité.
Pour ceux qui sont arrivés au bout de la série d’articles, bravo ! N’hésitez pas à me laisser un commentaire si ces posts vous ont appris quelque chose ou si vous avez des remarques !