Orthogonalité des tests unitaires vs concision des tests unitaires

14

J'écris des tests unitaires pour un système de direction pour un jeu vidéo. Le système a plusieurs comportements (éviter cette zone pour la raison A, éviter cette zone pour la raison B, chacun ajoutant un peu de contexte à une carte de la région. Une fonction distincte analyse ensuite la carte et produit le mouvement souhaité.

J'ai du mal à décider comment écrire les tests unitaires pour les comportements. Comme le suggère TDD, je ne m'intéresse qu'à la façon dont les comportements affectent le mouvement souhaité. Par exemple, éviter-pour-raison-A devrait entraîner un mouvement loin de la mauvaise position suggérée. Je me fiche de savoir comment ou pourquoi le comportement ajoute du contexte à la carte, mais seulement que le mouvement souhaité est éloigné de la position.

Donc, mes tests pour chaque comportement configurent le comportement, le font écrire sur la carte, puis exécutent la fonction d'analyse de carte pour déterminer le mouvement souhaité. Si ce mouvement satisfait mes spécifications, je suis content.

Cependant, maintenant mes tests dépendent à la fois des comportements qui fonctionnent correctement et de la fonction d'analyse de la carte qui fonctionnent correctement. Si la fonction d'analyse échoue, j'obtiendrais des centaines de tests ayant échoué plutôt que quelques. De nombreux guides de rédaction de tests suggèrent que c'est une mauvaise idée.

Cependant, si je teste directement par rapport à la sortie des comportements en se moquant de la carte, alors je me connecte sûrement trop étroitement à la mise en œuvre? Si je peux obtenir le même mouvement souhaité de la carte en utilisant un comportement légèrement différent, alors les tests devraient toujours réussir.

Alors maintenant, je souffre de dissonance cognitive. Quelle est la meilleure façon de structurer ces tests?

tenpn
la source
... pas sûr qu'il y ait une réponse magique. Fondamentalement, vous testez des choses qui peuvent casser. Donc, idéalement, vous pourriez d'une manière ou d'une autre, par magie, savoir quels tests de bas niveau étaient déjà suffisamment couverts par des tests de haut niveau pour qu'ils n'aient pas besoin de leurs propres tests unitaires de niveau inférieur. Une autre façon de voir les choses est la suivante: entre le moment où un test échoue et celui où un développeur résout le problème, combien de temps s'écoulera? Vous voulez que ce temps soit bas. Si vous ne faisiez pas d'unités, mais seulement des tests fonctionnels parfaits (couverture complète de haut niveau), ce temps serait encore trop long. Essayez de l'utiliser comme un guide heuristique.
Calphool

Réponses:

10

Dans le monde idéal, vous auriez en effet un ensemble de tests unitaires parfaitement orthogonaux, tous au même niveau d'abstraction.

Dans le monde réel, vous avez généralement des tests sur de nombreux niveaux différents de l'application, donc souvent les tests de niveau supérieur exercent une fonctionnalité qui a déjà été testée par des tests de niveau inférieur dédiés. (Beaucoup de gens préfèrent appeler ces tests de niveau supérieur sous-système / tests d'intégration plutôt que des tests unitaires; néanmoins, ils peuvent toujours fonctionner sur le même cadre de tests unitaires, donc du point de vue technique, il n'y a pas beaucoup de différence.)

Je ne pense pas que ce soit une mauvaise chose. Le but est de faire tester votre code de la meilleure façon qui corresponde à votre projet et à votre situation, de ne pas adhérer à "la voie idéale".

Péter Török
la source
Je suppose que le point principal de l'auteur était de savoir si vous devez utiliser des stubs / mocks ou de vraies implémentations pour ces tests de niveau supérieur
SiberianGuy
2

Ce type de test est la raison pour laquelle les simulateurs ont été inventés. L'idée principale: vous écrivez une maquette pour votre objet (carte, comportement, personnage, ...), puis vous écrivez des tests en utilisant cette maquette au lieu de l'objet réel. Les gens appellent parfois des faux moignons et je crois qu'il y a d'autres mots pour les deux.

Dans votre cas, vous écririez une maquette pour la carte chaque fois que vous aurez besoin de tester les comportements, et d'autres simulations pour les comportements chaque fois que vous voudrez tester la carte. Vos simulations seraient idéalement beaucoup plus simples que les comportements réels que vous modifiez, n'incorporant que les méthodes ou les variables dont vous avez réellement besoin pour ce test. Vous devrez peut-être écrire une maquette différente pour chaque test, ou vous pourrez réutiliser certaines simulations. Quoi qu'il en soit, ils devraient être adaptés au test et ne devraient pas essayer de ressembler autant que possible au comportement réel.

Si vous incluez des exemples de cartes ou de comportements, peut-être que quelqu'un pourrait fournir des exemples de simulations que vous pourriez écrire. Pas moi, car je n'ai jamais programmé un jeu vidéo plus avancé que Pong, et même alors, je suivais un livre, mais peut-être quelqu'un qui connaît bien les tests unitaires et le développement de jeux.

trysis
la source
0

Je pense que vous essayez de tester des trucs qui sont d'un niveau beaucoup plus élevé qu'une unité.

La plupart des tests de comportement nécessiteraient une certaine intelligence derrière, pour savoir s'il s'est comporté correctement. Cela ne peut pas être fait facilement à l'aide de tests automatisés.

oenone
la source
c'est pourquoi j'ai mentionné la concision. Je peux très facilement écrire de petits tests unitaires pour tester des lignes de code individuelles dans un comportement, et je l'ai fait, mais cela dépend de la lecture de la sortie de la fonction d'analyse de carte. Aucun de mes tests de comportement n'est volumineux, chacun ne teste qu'une seule fonctionnalité du comportement.
tenpn