Comment testez-vous un codeur à l'unité?

9

J'ai quelque chose comme ça:

public byte[] EncodeMyObject(MyObject obj)

J'ai fait des tests unitaires comme celui-ci:

byte[] expectedResults = new byte[3]{ 0x01, 0x02, 0xFF };
Assert.IsEqual(expectedResults, EncodeMyObject(myObject));

EDIT: Les deux façons que j'ai vues proposées sont:

1) Utilisation de valeurs attendues codées en dur, comme dans l'exemple ci-dessus.

2) Utiliser un décodeur pour décoder le tableau d'octets codés et comparer les objets d'entrée / sortie.

Le problème que je vois avec la méthode 1 est qu'elle est très fragile et nécessite beaucoup de valeurs codées en dur.

Le problème avec la méthode 2 est que le test de l'encodeur dépend du bon fonctionnement du décodeur. Si l'encodeur / décodeur sont cassés de manière égale (au même endroit), alors les tests pourraient produire des faux positifs.

Ce sont peut-être bien les seuls moyens de tester ce type de méthode. Si tel est le cas, alors très bien. Je pose la question pour voir s'il existe de meilleures stratégies pour ce type de test. Je ne peux pas révéler les internes de l'encodeur particulier sur lequel je travaille. Je demande en général comment vous pourriez résoudre ce type de problème, et je ne pense pas que les internes soient importants. Supposons qu'un objet d'entrée donné produira toujours le même tableau d'octets de sortie.

ConditionRacer
la source
4
Comment myObjectva de myObjectà { 0x01, 0x02, 0xFF }? Cet algorithme peut-il être décomposé et testé? La raison pour laquelle je demande est actuellement, il semble que vous ayez un test qui prouve qu'une chose magique produit une autre chose magique. Votre seule confiance est que la seule entrée produit la seule sortie. Si vous pouvez décomposer l'algorithme, vous pouvez gagner davantage en confiance dans l'algorithme et être moins dépendant des entrées et sorties magiques.
Anthony Pegram
3
@Codism Et si l'encodeur et le décodeur sont cassés au même endroit?
ConditionRacer
2
Les tests consistent, par définition, à faire quelque chose et à vérifier si vous avez obtenu les résultats attendus, ce que fait votre test. Vous devez, bien sûr, vous assurer de faire suffisamment de tests comme celui-ci pour vous assurer que vous exercez tous vos cas de code et de couverture et autres bizarreries.
Blrfl
1
@ Justin984, eh bien, maintenant nous allons plus loin. Je n'exposerais pas ces internes privés en tant que membres de l'API de l'encodeur, certainement pas. Je les supprimerais complètement de l'encodeur. Ou plutôt, l'Encodeur déléguerait ailleurs, une dépendance . Si c'est une bataille entre une méthode de monstre non testable ou un tas de classes d'assistance, je choisis les classes d'assistance à chaque fois. Mais encore une fois, je fais des inférences non informées à votre code à ce stade, car je ne le vois pas. Mais si vous voulez gagner en confiance dans vos tests, avoir des méthodes plus petites qui font moins de choses est un moyen d'y arriver.
Anthony Pegram
1
@ Justin984 Si la spécification change, vous modifiez la sortie attendue dans votre test et elle échoue maintenant. Ensuite, vous modifiez la logique de l'encodeur pour passer. Semble exactement comment TDD est censé fonctionner et il échouera seulement quand il le devrait. Je ne vois pas comment cela le rend fragile.
Daniel Kaplan

Réponses:

1

Vous êtes dans une situation désagréable là-bas. Si vous aviez un format statique dans lequel vous encodiez, votre première méthode serait la voie à suivre. Si ce n'était que votre propre format, et que personne d'autre n'avait à décoder, la deuxième méthode serait la voie à suivre. Mais vous ne rentrez vraiment dans aucune de ces catégories.

Ce que je ferais, c'est essayer de décomposer les choses en fonction du niveau d'abstraction.

Donc, je commencerais par quelque chose au niveau du bit, que je testerais quelque chose comme

bitWriter = new BitWriter();
bitWriter.writeInt(42, bits = 7);
assertEqual( bitWriter.data(), {0x42} )

L'idée est donc que le bitwriter sait comment écrire les types de champs les plus primitifs, comme les entiers.

Des types plus complexes seraient mis en œuvre en utilisant et testé quelque chose comme:

bitWriter = new BitWriter();
writeDate(bitWriter, new Datetime(2001, 10, 4));

bitWriter2 = new BitWriter();
bitWriter2.writeInt(2001, 12)
bitWriter2.writeInt(10, 4)
bitWriter2.writeInt(4, 6)

assertEquals(bitWriter.data(), bitWriter2.data() )

Notez que cela évite de savoir comment les bits réels sont emballés. Cela a été testé par le test précédent, et pour ce test, nous supposerons à peu près que cela fonctionne.

Ensuite, au niveau d'abstraction suivant, nous aurions

bitWriter = new BitWriter();
encodeObject(bitWriter, myObject);


bitWriter2 = new BitWriter();
bitWriter2.writeInt(42, 32)
writeDate(bitWriter2, new Datetime(2001, 10, 4));
writeVarString(bitWriter2, "alphanumeric");

assertEquals(bitWriter.data(), bitWriter2.data() )

donc, encore une fois, nous n'essayons pas d'inclure la connaissance de la façon dont les varstrings ou les dates ou les nombres sont réellement encodés. Dans ce test, nous ne sommes intéressés que par l'encodage produit par encodeObject.

Le résultat final est que si le format des dates est modifié, vous devrez corriger les tests qui impliquent réellement des dates, mais tous les autres codes et tests ne sont pas concernés par la façon dont les dates sont réellement encodées et une fois que vous mettez à jour le code pour le faire ce travail, tous ces tests passeront très bien.

Winston Ewert
la source
J'aime ça. Je suppose que c'est ce que certains des autres commentateurs ont dit à propos de le diviser en petits morceaux. Il n'évite pas complètement le problème lorsque la spécification change, mais il l'améliore.
ConditionRacer
6

Dépend. Si le codage est quelque chose de complètement fixe, où chaque implémentation est censée créer exactement la même sortie, cela n'a aucun sens de vérifier autre chose que de vérifier que les exemples d'entrées correspondent exactement aux sorties attendues. C'est le test le plus évident, et probablement aussi le plus facile à écrire.

S'il y a une marge de manœuvre avec des sorties alternatives, comme dans la norme MPEG (par exemple, vous pouvez appliquer certains opérateurs à l'entrée, mais vous êtes libre de faire un compromis entre l'effort d'encodage et la qualité de sortie ou l'espace de stockage), il est préférable d'appliquer la défini la stratégie de décodage de la sortie et vérifiez qu'elle est identique à l'entrée - ou, si l'encodage est avec perte, qu'elle est raisonnablement proche de l'entrée d'origine. C'est plus difficile à programmer, mais vous protège contre toute amélioration future qui pourrait être apportée à votre encodeur.

Kilian Foth
la source
2
Supposons que vous utilisez le décodeur et comparez les valeurs. Que faire si l'encodeur et le décodeur sont tous les deux tombés en panne au même endroit? L'encodeur n'encode pas correctement et le décodeur ne décode pas correctement, mais les objets d'entrée / sortie sont corrects car le processus a été mal effectué deux fois.
ConditionRacer
@ Justin984 utiliser alors que l' on appelle des « vecteurs de test », entrée / savoir paires de sorties que vous pouvez utiliser pour tester avec précision un codeur et décodeur
cliquet monstre
@ratchet freak Cela me remet à tester avec les valeurs attendues. Ce qui est bien, c'est ce que je fais actuellement, mais c'est un peu fragile alors je cherchais s'il y avait de meilleures façons.
ConditionRacer
1
Mis à part la lecture attentive de la norme et la création d'un cas de test pour chaque règle, il n'y a pratiquement aucun moyen d'éviter qu'un codeur et un décodeur contiennent le même bogue. Par exemple, supposons que "ABC" doit être traduit en "xyz" mais l'encodeur ne le sait pas et votre décodeur ne comprendrait pas non plus "xyz" s'il le rencontrait jamais. Les cas de test fabriqués à la main ne contiennent pas la séquence "ABC", car le programmeur n'était pas au courant de cette règle, et également un test avec encodage / décodage de chaînes aléatoires passerait incorrectement parce que l'encodeur et le décodeur ignorent le problème.
user281377
1
Pour aider à détecter les bogues affectant à la fois les décodeurs et les encodeurs écrits par vous-même en raison de connaissances manquantes, faites un effort pour obtenir des sorties d'encodeur auprès d'autres fournisseurs; et essayez également de tester la sortie de votre encodeur sur les décodeurs tiers. Il n'y a pas d'alternative autour d'elle.
rwong
3

Testez cela encode(decode(coded_value)) == coded_valueet decode(encode(value)) == value. Vous pouvez donner une entrée aléatoire aux tests si vous le souhaitez.

Il est toujours possible que l'encodeur et le décodeur soient cassés de manière complémentaire, mais cela semble assez peu probable à moins que vous ayez une mauvaise compréhension conceptuelle de la norme d'encodage. Faire des tests codés en dur de l'encodeur et du décodeur (comme vous le faites déjà) devrait éviter cela.

Si vous avez accès à une autre implémentation de celle-ci qui fonctionne, vous pouvez au moins l'utiliser pour avoir l'assurance que votre implémentation est bonne même si son utilisation dans les tests unitaires serait impossible.

Michael Shaw
la source
Je suis d'accord qu'une erreur d'encodeur / décodeur complémentaire est peu probable en général. Dans mon cas spécifique, le code des classes encodeur / décodeur est généré par un autre outil basé sur les règles d'une base de données. Des erreurs complémentaires se produisent donc occasionnellement.
ConditionRacer
Comment peut-il y avoir des «erreurs complémentaires»? Cela implique qu'il existe une spécification externe pour la forme codée, et donc un décodeur externe.
kevin cline
Je ne comprends pas votre utilisation du mot externe. Mais il existe une spécification pour la façon dont les données sont encodées et également un décodeur. Une erreur complémentaire est lorsque l'encodeur et le décodeur fonctionnent tous les deux de manière complémentaire mais qui s'écarte de la spécification. J'ai un exemple dans les commentaires sous la question d'origine.
ConditionRacer
Si l'encodeur était censé implémenter ROT13 mais accidentellement ROT14 et le décodeur aussi, alors décodez (encoder ('a')) == 'a' mais l'encodeur est toujours cassé. Pour des choses beaucoup plus compliquées que cela, il est probablement beaucoup moins probable que ce genre de chose se produise, mais théoriquement cela pourrait arriver.
Michael Shaw
@MichaelShaw juste une anecdote, l'encodeur et le décodeur pour ROT13 sont les mêmes; ROT13 est son propre inverse. Si vous avez implémenté ROT14 par erreur, decode(encode(char))ce ne serait pas égal char(ce serait égal char+2).
Tom Marthenal
2

Testez les exigences .

Si les exigences ne sont que «encoder dans un flux d'octets qui, une fois décodé, produit un objet équivalent», testez simplement l'encodeur par décodage. Si vous écrivez à la fois l'encodeur et le décodeur, testez-les ensemble. Ils ne peuvent pas avoir des "erreurs de correspondance". S'ils travaillent ensemble, le test réussit.

S'il existe d'autres exigences pour le flux de données, vous devrez les tester en examinant les données codées.

Si le format codé est prédéfini, alors vous devrez vérifier les données codées par rapport au résultat attendu, comme vous l'avez fait, ou (mieux) obtenir un décodeur de référence auquel vous pouvez faire confiance pour effectuer la vérification. L'utilisation d'un décodeur de référence élimine la possibilité que vous ayez mal interprété la spécification de format.

kevin cline
la source
1

Selon le cadre de test et le paradigme que vous utilisez, vous pouvez toujours utiliser le modèle Arrange Act Assert pour cela, comme vous l'avez dit.

[TestMethod]
public void EncodeMyObject_ForValidInputs_Encodes()
{
    //Arrange object under test
    MyEncoder encoderUnderTest = new MyEncoder();
    MyObject validObject = new MyOjbect();
    //arrange object for condition under test

    //Act
    byte[] actual = encoderUnderTest.EncodeMyObject(myObject);

    //Assert
    byte[] expected = new byte[3]{ 0x01, 0x02, 0xFF };
    Assert.IsEqual(expected, actual);
}

Vous devez connaître les exigences EncodeMyObject()et pouvez utiliser ce modèle pour tester par rapport à chacun d'eux des critères valides et invalides, en organisant chacun d'eux et en codant en dur le résultat attendu pour expected, de même pour le décodeur.

Étant donné que les attendus sont codés en dur, ils seront fragiles si vous avez un changement massif.

Vous pouvez être en mesure d'automatiser avec quelque chose de paramétré (jetez un œil à Pex ) ou si vous faites DDD ou BDD jetez un œil à gerkin / concombre .

StuperUser
la source
1

Vous décidez de ce qui est important pour vous.

Est-il important pour vous qu'un objet survive à l'aller-retour et que le format de fil exact ne soit pas vraiment important? Ou le format de fil exact est-il une partie importante de la fonctionnalité de votre encodeur et décodeur?

Si c'est le premier, assurez-vous simplement que les objets survivent au voyage aller-retour. Si l'encodeur et le décodeur sont tous deux cassés de manière exactement complémentaire, vous ne vous en souciez pas vraiment.

Dans ce dernier cas, vous devez tester que le format de fil est celui que vous attendez pour les entrées données. Cela signifie soit tester le format directement, soit utiliser une implémentation de référence. Mais après avoir testé les bases, vous pouvez tirer profit de tests aller-retour supplémentaires, qui devraient être plus faciles à écrire en volume.

Bill Michell
la source