paint-brush
Comment améliorer le code et éviter les bagarres lors de la révisionpar@zavodnoyapl
1,821 lectures
1,821 lectures

Comment améliorer le code et éviter les bagarres lors de la révision

par Aleksei Dovbenko23m2024/02/11
Read on Terminal Reader

Trop long; Pour lire

Tests unitaires et paresse : une méthode sans contestation pour booster les compétences individuelles au sein des équipes tech.
featured image - Comment améliorer le code et éviter les bagarres lors de la révision
Aleksei Dovbenko HackerNoon profile picture
0-item
1-item


Ce n'est un secret pour personne que lorsqu'il s'agit de former une nouvelle équipe, les dirigeants (Team Leader, Tech Leader) sont confrontés au défi d'établir un style de programmation unifié, car tous les membres de l'équipe sont nouveaux et chacun a sa propre approche pour organiser le code et sélectionner. les pratiques. Généralement, cela conduit à de longs débats lors des révisions de code, qui finissent par dégénérer en diverses interprétations de pratiques bien connues telles que SOLID, KISS, DRY, etc. Les principes derrière ces pratiques sont assez flous, et avec suffisamment de persévérance, il est facile de trouver paradoxes où l’un contredit l’autre. Par exemple, considérons la responsabilité unique et DRY.


Une variante de la définition du principe de responsabilité unique (le « S » dans SOLID) stipule que chaque objet doit avoir une responsabilité et que cette responsabilité doit être entièrement encapsulée dans la classe. Le principe DRY (Don't Repeat Yourself) suggère d'éviter la duplication de code. Cependant, si nous avons un objet de transfert de données (DTO) dans notre code qui peut être utilisé dans différentes couches/services/modules, lequel de ces principes devons-nous suivre ? Sans aucun doute, de nombreux livres de programmation abordent des situations similaires, déclarant généralement que si nous avons affaire à des objets/fonctions différents avec le même ensemble de propriétés et de logique mais appartenant à des domaines différents, cela ne constitue pas une duplication. Cependant, comment prouver que ces objets DEVRAIENT appartenir à des domaines différents et, plus important encore, le leader est-il prêt (et confiant) à affirmer et à prouver cette affirmation ?

 One frequently practiced approach is making categorical statements like "This is our way/It's the leader's word and we take it for granted" and similar authoritative declarations that emphasize the authority and expertise of the person who came up with these rules. This approach undoubtedly succeeds when dealing with an established team and a project with an existing codebase upon which development continues. But what should be done when the team is new, and the project has just begun? Appeals to authority may not work, as the Team/Tech Leader has not yet established their authority, and each team member believes that their knowledge and approach will be the optimal solution for the future project.


Cet article propose une approche qui permet d’éviter la plupart de ces situations litigieuses. De plus, chaque développeur en pratique (sans objections de la part du leader) comprendra ce qu'il fait de mal et comment l'améliorer.


Pour commencer, introduisons plusieurs conditions et définitions supplémentaires :

  1. Au moment de la soumission pour révision, la tâche est considérée comme terminée et si elle réussit la révision, elle peut être publiée sans aucune modification. En d’autres termes, nous ne considérons pas la possibilité de modifications/ajouts pré-planifiés dans le code.

  2. L'équipe est composée de spécialistes tout aussi expérimentés et qualifiés qui ne rencontrent aucun problème dans la mise en œuvre des tâches ; la seule différence réside dans leurs approches.

  3. Le style de code est cohérent et est vérifié par des vérificateurs de code.

  4. Le temps de développement n'est pas critique, du moins moins critique que la fiabilité du produit.


    Nous examinerons plus loin la nécessité de la première condition, même si elle est assez évidente en soi, car il est illogique de soumettre à l'examen une tâche inachevée. Avec la deuxième condition, nous garantissons que chaque membre de l'équipe n'a aucun problème à choisir un algorithme et à mettre en œuvre la tâche assignée. Dans la troisième condition, nous supposons que l'équipe adhère à un style spécifique (PSR), et des questions telles que « qu'est-ce qui est le mieux, CamelCase ou Snake_case » ne se posent pas. Et la condition finale s’abstient de calculer les changements d’effort pour accomplir la tâche dans ce travail.

Tests unitaires

De nombreux lecteurs savent que les tests unitaires améliorent la qualité du code. Généralement, après avoir déclaré cela, la méthodologie de développement piloté par les tests (TDD) est mentionnée à titre d'exemple, qui améliore effectivement la qualité du code mais est relativement rarement appliquée dans la pratique car l'écriture de tests avant la mise en œuvre nécessite un ensemble de compétences en programmation de haut niveau.


Comment les tests unitaires peuvent-ils aider à améliorer le code sans s’appuyer sur les pratiques bien connues mentionnées précédemment ? Tout d'abord, rappelons que les tests unitaires sont appliqués pour tester une méthode/un module/une classe spécifique en utilisant des objets/modules fictifs comme dépendances.


Suite à la première condition, la tâche doit être considérée comme terminée au moment de sa soumission pour examen. Par conséquent, introduisons une définition de ce que nous considérons comme une tâche terminée. Une tâche est réputée achevée seulement lorsqu’elle satisfait à toutes les conditions énumérées ci-dessous :

  • Les exigences de la tâche assignée sont remplies.

  • Tout nouveau code doit être couvert par des tests unitaires, y compris diverses conditions algorithmiques du programme.

  • Le nouveau code ne rompt pas les tests existants.

    Puisque nous disposons d'un temps illimité pour écrire de nouveaux tests et maintenir les anciens (condition 4), et que chaque développeur peut écrire ces tests et répondre aux exigences de la tâche (condition 2), nous pouvons considérer que n'importe quelle tâche peut potentiellement être complétée. Maintenant que nous avons introduit la définition d'une tâche terminée, nous pouvons justifier la condition 1 : le code ne peut pas être soumis en révision s'il n'est pas couvert par des tests ; sinon, le code sera rejeté sans examen. Par conséquent, un développeur sait que résoudre les problèmes de code après les commentaires implique de corriger les tests. Ce point apparemment mineur devient un moteur fondamental pour écrire du bon code.


    Considérons l'exemple de code suivant (dans cet article, le langage PHP est utilisé à titre d'exemple, mais il peut s'agir de n'importe quel langage de type C prenant en charge le paradigme de programmation orientée objet) :


 class SomeFactory { public function __construct( private readonly ARepository $aRepository, ) { } /** * @throws ErrorException */ public function createByParameters(ObjectType $type, array $parameters): ObjectE|ObjectD|ObjectA|ObjectB|ObjectC { switch ($type) { case ObjectType::A: if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); case ObjectType::B: // some code return new ObjectB($parameters); case ObjectType::C: // some code return new ObjectC($parameters); case ObjectType::D: // some code return new ObjectD($parameters); case ObjectType::E: // some code return new ObjectE($parameters); } throw new RuntimeException('some message'); } }


Ici, nous avons intentionnellement violé toutes les pratiques pour démontrer l'efficacité de l'approche proposée. Notez cependant que l’algorithme présenté est fonctionnel ; selon le type, une entité avec des paramètres spécifiques est créée. Néanmoins, notre tâche principale est de garantir que ce code n'atteigne pas le stade de la révision, incitant le développeur à l'améliorer de manière indépendante. Suite à la condition 1, pour soumettre le code pour révision, nous devons écrire des tests. Écrivons un de ces tests :


 class SomeFactoryTest extends TestCase { public function testCreateByParametersReturnsObjectAWithDefaultMethods(): void { $someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), ); $parameters = [ 'id' => $id = 5, 'default' => ['someData'], ]; $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null); $actualResult = $someFactory->createByParameters(ObjectType::A, $parameters); $this->assertInstanceOf(ObjectA::class, $actualResult); // additional checkers for $actualResult } }


Cela s'est avéré assez simple, mais ce n'est qu'un des huit tests nécessaires pour l'un des cinq types. Une fois tous les tests écrits, tout commentaire lors de la révision nécessitant des modifications peut interrompre ces tests et le développeur devra les réécrire ou les ajuster. Par exemple, l'ajout d'une nouvelle dépendance (disons, un enregistreur) entraînera des modifications de l'initialisation d'usine dans tous les tests :


 $someFactory = new SomeFactory( $aRepository = $this->createMock(ARepository::class), $this->createMock(LoggerInterface::class) );


Notez comment le coût d'un commentaire a augmenté : si auparavant l'ajout/la modification d'une dépendance nécessitait uniquement des modifications de la classe SomeFactory , désormais tous les tests (qui pouvaient être plus de 40) devront également être modifiés. Naturellement, après plusieurs itérations de tels changements, un développeur souhaiterait minimiser les efforts requis pour répondre aux commentaires. Comment cela peut-il être fait? La réponse est évidente : isoler la logique de création d'entités pour chaque type dans une classe distincte. Veuillez noter que nous ne nous appuyons pas sur les principes SOLID/DRY, etc., et que nous ne nous engageons pas dans des discussions abstraites sur la lisibilité du code et le débogage, car chacun de ces arguments peut être contesté. Nous simplifions simplement l'écriture des tests, et il n'y a aucun contre-argument pour un développeur contre cela.


Après la modification, nous aurons 5 usines pour chaque type ( ObjectType::A , ObjectType::B , ObjectType::C , ObjectType::D , ObjectType::E ). Vous trouverez ci-dessous un exemple de fabrique pour ObjectType::A (FactoryA) :

 class FactoryA { public function __construct( private readonly ARepository $aRepository, ) { } public function createByParameters(array $parameters): ObjectA { if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { // 6 7 $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


Et l'usine générale ressemblera à ceci :


 class SomeFactory { public function __construct( private readonly FactoryA $factoryA, private readonly FactoryB $factoryB, private readonly FactoryC $factoryC, private readonly FactoryD $factoryD, private readonly FactoryE $factoryE, ) { } /** * @throws ErrorException */ public function createByParameters(ObjectType $type, array $parameters): ObjectE|ObjectD|ObjectA|ObjectB|ObjectC { switch ($type) { case ObjectType::A: return $this->factoryA->createByParameters($parameters); case ObjectType::B: return $this->factoryB->createByParameters($parameters); case ObjectType::C: return $this->factoryC->createByParameters($parameters); case ObjectType::D: return $this->factoryD->createByParameters($parameters); case ObjectType::E: return $this->factoryE->createByParameters($parameters); } throw new RuntimeException('some message'); } }


Comme nous pouvons le constater, le code global a augmenté. Examinons les tests pour FactoryA et le test modifié pour SomeFactory .


 class FactoryATest extends TestCase { public function testCreateByParametersReturnsObjectAWithDefaultMethods(): void { $factoryA = new FactoryA( $aRepository = $this->createMock(ARepository::class), ); $parameters = [ 'id' => $id = 5, 'default' => ['someData'], ]; $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null); $actualResult = $factoryA->createByParameters($parameters); $this->assertInstanceOf(ObjectA::class, $actualResult); // additional checkers for $actualResult } }


 class SomeFactoryTest extends TestCase { public function testCreateByParametersReturnsObjectA(): void { $someFactory = new SomeFactory( $factoryA = $this->createMock(FactoryA::class), $this->createMock(FactoryB::class), $this->createMock(FactoryC::class), $this->createMock(FactoryD::class), $this->createMock(FactoryE::class), ); $parameters = ['someParameters']; $factoryA->expects($this->once()) ->method('createByParameters') ->with($parameters) ->willReturn($objectA = $this->createMock(ObjectA::class)); $this->assertSame($objectA, $someFactory->createByParameters(ObjectType::A, $parameters)); } // the same test for another types and fabrics }


Le nombre total de tests a augmenté de 5 (le nombre de types possibles), tandis que le nombre de tests pour les usines est resté le même. Alors, qu’est-ce qui rend ce code meilleur ? Le principal avantage est la réduction des efforts requis pour les corrections après une révision du code. En effet, lors du changement de dépendances dans FactoryA , seuls les tests pour FactoryA sont concernés.


Nous sommes d’accord, le code semble déjà meilleur et, peut-être involontairement, nous avons en partie adhéré au principe de responsabilité unique. Est-ce la fin ? Comme mentionné précédemment, nous devons encore écrire 5 tests pour chaque entité. De plus, nous devrions sans cesse transmettre les usines au constructeur comme arguments pour ce service, et l'introduction d'un nouveau type (ou la suppression d'un ancien) entraînerait des changements dans tous les tests (bien qu'ils ne soient plus que 5) pour SomeFactory . Par conséquent, une solution logique, que la plupart des développeurs verront probablement, consiste à créer un registre (surtout s'il existe un support natif pour l'enregistrement des classes par interface) et à déclarer des interfaces pour les DTO et les usines telles que :


 interface ObjectInterface { } class ObjectA implements ObjectInterface { // some logic }


 interface FactoryInterface { public function createByParameters(array $parameters): ObjectInterface; public static function getType(): ObjectType; }


 class FactoryB implements FactoryInterface { public static function getType(): ObjectType { return ObjectType::B; } public function createByParameters(array $parameters): ObjectB { // some logic return new ObjectB($parameters); } }


Soulignons le choix de définir la méthode getType comme statique. Dans l’implémentation actuelle, il n’y a aucune différence si cette méthode est statique ou dynamique. Cependant, si l’on commence à écrire un test pour cette méthode (aussi absurde que puisse paraître cette idée), on remarquera que dans le cas d’une méthode dynamique, le test ressemblerait à :


 public function testGetTypeReturnsTypeA(): void { $mock = $this->getMockBuilder(FactoryA::class) ->disableOriginalConstructor() ->onlyMethods([]) ->getMock(); $this->assertSame($mock->getType(), ObjectType::A); }


Alors que pour une méthode statique, cela semblerait beaucoup plus court :


 public function testGetTypeReturnsTypeA(): void { $this->assertSame(FactoryA::getType(), ObjectType::A); }


Ainsi, grâce à la paresse, nous avons choisi la bonne solution (peut-être sans le savoir) et empêché la méthode getType de dépendre potentiellement de l'état de l'objet de la classe FactoryB .


Regardons le code du registre :


 class SomeRegistry { /** @var array<int, FactoryInterface> */ private readonly array $factories; /** * @param FactoryInterface[] $factories */ public function __construct(array $factories) { $mappedFactory = []; foreach ($factories as $factory) { if (array_key_exists($factory::getType()->value, $mappedFactory)) { throw new RuntimeException('Duplicate message'); } $mappedFactory[$factory::getType()->value] = $factory; } $this->factories = $mappedFactory; } public function createByParams(ObjectType $type, array $parameters): ObjectInterface { $factory = $this->factories[$type->value] ?? null; if ($factory === null) { throw new RuntimeException('Not found exception'); } return $factory->createByParameters($parameters); } }

Comme on peut le voir, nous devons écrire 3 tests : 1) un test de duplication, 2) un test lorsque l'usine n'est pas trouvée, et 3) un test lorsque l'usine est trouvée. La classe SomeFactory ressemble désormais à une méthode proxy et peut donc être supprimée.


 class SomeFactory { public function __construct( private readonly SomeRegistry $someRegistry, ) { } public function createByParameters(ObjectType $type, array $parameters): ObjectInterface { return $this->someRegistry->createByParams($type, $parameters); } }


Outre la réduction du nombre de tests (de 5 à 3), tout ajout/suppression d'une nouvelle usine n'entraîne pas de modification des anciens tests (en supposant que l'enregistrement des nouvelles usines soit natif et intégré au framework).


Pour résumer nos progrès : dans la recherche d'une solution permettant de réduire le coût de traitement des commentaires après une revue de code, nous avons complètement repensé la génération d'objets en fonction des types. Notre code adhère désormais aux principes de responsabilité unique et d'ouverture/fermeture (le « S » et le « O » dans l'acronyme SOLID), même si nous ne les avons explicitement mentionnés nulle part.


Ensuite, rendons la tâche plus complexe et effectuons le même travail avec des modifications moins évidentes dans le code. Examinons le code de la classe FactoryA :


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); } $aEntity = $this->aRepository->findById($parameters['id']); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


Pouvons-nous simplifier l’écriture des tests pour ce code ? Décomposons le premier bloc if :


 if (!array_key_exists('id', $parameters) || !is_int($parameters['id'])) { throw new ErrorException('Some message'); }


Essayons de le couvrir avec des tests :


 public function testCreateByParametersThrowsErrorExceptionWhenParameterIdDoesntExist(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), ); $factoryA->createByParameters([]); } public function testCreateByParametersThrowsErrorExceptionWhenParameterIdNotInt(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), ); $factoryA->createByParameters(['id' => 'test']); }


Si la question de l'existence est facilement abordée, le test du type comporte de nombreux pièges. Dans ce test, nous avons réussi une chaîne, mais qu'en est-il des autres types ? Un grand nombre est-il considéré comme un entier ou un nombre à virgule flottante (par exemple, en PHP, 10 à la puissance 100 renverra une représentation courte comme 1.0E+100 de type float) ? Vous pouvez écrire un DataProvider pour tous les scénarios possibles, ou vous pouvez extraire la logique de validation dans une classe distincte et obtenir quelque chose comme :


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); try { $id = $extractor->getIntByKey('id'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } $aEntity = $this->aRepository->findById($id); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } } return new ObjectA($data); } }


D’une part, nous avons ajouté une nouvelle dépendance, et peut-être avons-nous même dû la créer. Mais en échange, dans toutes les autres usines, nous n’avons pas à nous soucier de tels problèmes. Le test dans l'usine actuelle n'en est qu'un, et il couvre toutes les variations possibles du paramètre id :


 public function testCreateByParametersThrowsErrorExceptionWhenParameterIdDoesntExist(): void { $this->expectException(ErrorException::class); $factoryA = new FactoryA( $this->createMock(ARepository::class), $extractorFactory = $this->createMock(ExtractorFactory::class), ); $parameters = ['someParameters']; $extractorFactory->expects($this->once()) ->method('createByArray') ->with($parameters) ->willReturn($extractor = $this->createMock(Extractor::class)); $extractor->expects($this->once()) ->method('getIntByKey') ->with('id') ->willThrowException($this->createMock(ExtractorException::class)); $factoryA->createByParameters($parameters); }


Regardons le bloc de code suivant, à savoir :


 $aEntity = $this->aRepository->findById($id); $data = []; if ($aEntity !== null) { $data = $aEntity->getSomeParams(); } if (count($data) === 0) { // next code


Dans ce bloc, la méthode de la dépendance aRepository ( findById ) est appelée, qui renvoie soit null, soit une entité avec la méthode getSomeParams . La méthode getSomeParams , à son tour, renvoie un tableau de données.


Comme nous pouvons le voir, la variable $aEntity n'est nécessaire que pour appeler la méthode getSomeParams . Alors, pourquoi ne pas obtenir le résultat de getSomeParams directement s'il existe et un tableau vide s'il n'existe pas ?


 $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) {


Comparons les tests avant et après. Avant les changements, nous avions 3 comportements possibles : 1) lorsque l'entité est trouvée et que getSomeParams renvoie un tableau de données non vide, 2) lorsque l'entité est trouvée et que getSomeParams renvoie un tableau de données vide, 3) lorsque le l'entité n'est pas trouvée.


 // case 1 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn($this->createConfiguredMock(SomeEntity::class, [ 'getSomeParams' => ['not empty params'] ])); // case 2 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn($this->createConfiguredMock(SomeEntity::class, [ 'getSomeParams' => [] ])); // case 3 $aRepository->expects($this->once()) ->method('findById') ->with($id) ->willReturn(null);


Dans le code modifié, il n'y a que deux scénarios possibles : findSomeParamsById renvoie un tableau vide ou non.


 // case 1 $aRepository->expects($this->once()) ->method('findSomeParamsById') ->with($id) ->willReturn([]); // case 2 $aRepository->expects($this->once()) ->method('findSomeParamsById') ->with($id) ->willReturn(['not empty params']);


En plus de réduire le nombre de tests, nous nous sommes débarrassés de $this->createConfiguredMock(SomeEntity::class, [..] .
Ensuite, regardons le bloc :


 if (count($data) === 0) { if (array_key_exists('default', $parameters) && is_array($parameters['default'])) { $data = $parameters['default']; } else { throw new ErrorException('Some message'); } }


Puisque nous disposons déjà d'une classe capable d'extraire les données du type requis, nous pouvons l'utiliser en supprimant les vérifications du code d'usine :


 if (count($data) === 0) { try { $data = $extractor->getArrayByKey('default'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } }


Au final, nous obtenons une classe comme :


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); try { $id = $extractor->getIntByKey('id'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) { try { $data = $extractor->getArrayByKey('default'); } catch (ExtractorException $extractorException) { throw new ErrorException('Some message', previous: $extractorException); } } return new ObjectA($data); } }


La méthode createByParameters n'aura que 4 tests, à savoir :

  • un test pour la première exception ( getIntByKey )
  • un test lorsque findSomeParamsById a renvoyé un résultat non vide
  • un test lorsque findSomeParamsById a renvoyé un résultat vide et que la deuxième exception ( getArrayByKey ) est déclenchée
  • un test lorsque findSomeParamsById a renvoyé un résultat vide et ObjectA a été créé avec les valeurs du tableau default

Cependant, si les exigences de la tâche le permettent et que ErrorException peut être remplacé par ExtractorException, le code sera encore plus court :


 class FactoryA implements FactoryInterface { public function __construct( private readonly ARepository $aRepository, private readonly ExtractorFactory $extractorFactory ) { } public static function getType(): ObjectType { return ObjectType::A; } /** * @throws ExtractorException */ public function createByParameters(array $parameters): ObjectA { $extractor = $this->extractorFactory->createByArray($parameters); $id = $extractor->getIntByKey('id'); $data = $this->aRepository->findSomeParamsById($id); if (count($data) === 0) { $data = $extractor->getArrayByKey('default'); } return new ObjectA($data); } }


Et il n'y aura que deux tests :

  • un test lorsque findSomeParamsById a renvoyé un résultat non vide

  • un test lorsque findSomeParamsById a renvoyé un résultat vide et ObjectA a été créé avec les valeurs du tableau default


Résumons le travail effectué.


Au départ, nous avions du code mal écrit qui nécessitait une couverture de tests. Étant donné que tout développeur a confiance en son code (jusqu'à ce que quelque chose plante avec une erreur), écrire des tests est une tâche longue et monotone que personne n'aime. La seule façon d’écrire moins de tests est de simplifier le code qui doit être couvert par ces tests. Au final, en simplifiant et en réduisant le nombre de tests, le développeur améliore le code, sans nécessairement suivre de pratiques théoriques spécifiques.