Les résultats attendus des tests unitaires devraient-ils être codés en dur?

30

Les résultats attendus d'un test unitaire doivent-ils être codés en dur, ou peuvent-ils dépendre de variables initialisées? Les résultats codés en dur ou calculés augmentent-ils le risque d'introduire des erreurs dans le test unitaire? Y a-t-il d'autres facteurs que je n'ai pas pris en compte?

Par exemple, lequel de ces deux est un format plus fiable?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: En réponse à la réponse de DXM, l'option 3 est-elle une solution préférée?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Hand-E-Food
la source
2
Faire les deux. Sérieusement. Les tests peuvent et doivent se chevaucher. Examinez également une sorte de tests basés sur les données si vous vous retrouvez face à des valeurs codées en dur.
Job
Je conviens que la troisième option est celle que j'aime utiliser. Je ne pense pas que l'option 1 nuirait puisque vous éliminez la manipulation lors de la compilation.
kwelch
Cependant, vos deux options utilisent le codage en dur et s'arrêteront si le test n'est pas exécuté sur C: \\
Qwertie

Réponses:

27

Je pense que la valeur attendue calculée donne des cas de test plus robustes et flexibles. De plus, en utilisant de bons noms de variables dans l'expression qui calculent le résultat attendu, il est beaucoup plus clair d'où le résultat attendu provient en premier lieu.

Cela dit, dans votre exemple spécifique, je ne ferais PAS confiance à la méthode "Softcoded" car elle utilise votre SUT (système sous test) comme entrée pour vos calculs. S'il y a un bogue dans MyClass où les champs ne sont pas correctement stockés, votre test passera réellement car votre calcul de valeur attendue utilisera la mauvaise chaîne, tout comme target.GetPath ().

Ma suggestion serait de calculer la valeur attendue là où cela a du sens, mais assurez-vous que le calcul ne dépend d'aucun code du SUT lui-même.

En réponse à la mise à jour du PO à ma réponse:

Oui, en fonction de mes connaissances mais de mon expérience quelque peu limitée en TDD, je choisirais l'option # 3.

DXM
la source
1
Bon point! Ne vous fiez pas à l'objet non vérifié dans le test.
Hand-E-Food
n'est-ce pas la duplication du code SUT?
Abyx
1
d'une certaine manière, mais c'est ainsi que vous vérifiez que SUT fonctionne. Si nous devions utiliser le même code et qu'il a été détruit, vous ne le sauriez jamais. Bien sûr, si pour effectuer le calcul, vous devez dupliquer beaucoup de SUT, alors peut-être que l'option # 1 deviendrait meilleure, juste coder en dur la valeur.
DXM
16

Et si le code était le suivant:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Votre deuxième exemple n'attraperait pas le bogue, mais le premier exemple le ferait.

En général, je déconseille le codage logiciel car il peut masquer les bogues. Par exemple:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Pouvez-vous repérer le problème? Vous ne feriez pas la même erreur dans une version codée en dur. Il est plus difficile d'obtenir des calculs corrects que des valeurs codées en dur. C'est pourquoi je préfère travailler avec des valeurs codées en dur que celles codées en soft.

Mais il y a des exceptions. Et si votre code doit s'exécuter sur Windows et Linux? Non seulement le chemin devra être différent, il devra utiliser des séparateurs de chemin différents! Calculer le chemin à l'aide de fonctions qui font abstraction de la différence entre peut avoir un sens dans ce contexte.

Winston Ewert
la source
J'entends ce que vous dites et cela me donne quelque chose à considérer. Le softcoding repose sur mes autres cas de test (tels que ConstructorShouldCorrectlyInitialiseFields). L'échec que vous décrivez sera référencé par l'échec d'autres tests unitaires.
Hand-E-Food du
@ Hand-E-Food, on dirait que vous écrivez des tests sur les méthodes individuelles de vos objets. Non. Vous devez écrire des tests qui vérifient l'exactitude de l'ensemble de votre objet et non des méthodes individuelles. Sinon, vos tests seront fragiles vis-à-vis des changements à l'intérieur de l'objet.
Winston Ewert
Je ne suis pas sûr de suivre. L'exemple que j'ai donné était purement hypothétique, un scénario facile à comprendre. J'écris des tests unitaires pour tester les membres publics des classes et des objets. Est-ce la bonne façon de les utiliser?
Hand-E-Food du
@ Hand-E-Food, si je vous comprends bien, votre test ConstructShouldCorrectlyInitialiseFields invoquerait le constructeur, puis affirmerait que les champs sont correctement définis. Mais tu ne devrais pas faire ça. Vous ne devriez pas vous soucier de ce que font les champs internes. Vous devez seulement affirmer que le comportement externe de l'objet est correct. Sinon, le jour viendra où vous devrez remplacer l'implémentation interne. Si vous avez fait des affirmations sur l'état interne, tous vos tests se casseront. Mais si vous n'avez fait que des affirmations sur le comportement extérieur, tout fonctionnera toujours.
Winston Ewert
@ Winston - Je suis en train de parcourir le livre xUnit Test Patterns et avant de terminer The Art of Unit Testing. Je ne vais pas prétendre savoir de quoi je parle, mais j'aimerais penser que j'ai ramassé quelque chose dans ces livres. Les deux livres recommandent fortement que chaque méthode de test teste le minimum absolu et vous devriez avoir de nombreux cas de test pour tester l'ensemble de votre objet. De cette façon, lorsque les interfaces ou les fonctionnalités changent, vous ne devez vous attendre à corriger que quelques méthodes de test, plutôt que la plupart d'entre elles. Et comme ils sont petits, les changements devraient être plus faciles.
DXM
4

À mon avis, vos deux suggestions sont loin d'être idéales. La façon idéale de le faire est celle-ci:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

En d'autres termes, le test doit fonctionner exclusivement en fonction de l'entrée et de la sortie de l'objet, et non en fonction de l'état interne de l'objet. L'objet doit être traité comme une boîte noire. (J'ignore d'autres problèmes, comme le caractère inapproprié de l'utilisation de string.Join au lieu de Path.Combine, car ce n'est qu'un exemple.)

Mike Nakis
la source
1
Toutes les méthodes ne sont pas fonctionnelles - bon nombre d'entre elles ont correctement des effets secondaires qui modifient l'état de certains objets. Un test unitaire d'une méthode avec effets secondaires devrait probablement évaluer l'état des objets affectés par la méthode.
Matthew Flynn
Cet état serait alors considéré comme la sortie de la méthode. Le but de cet exemple de test est de vérifier la méthode GetPath (), pas le constructeur de MyClass. Lisez la réponse de @ DXM, il fournit une très bonne raison pour adopter l'approche de la boîte noire.
Mike Nakis
@MatthewFlynn, alors vous devriez tester les méthodes affectées par cet état. L'état interne exact est un détail d'implémentation et ne concerne pas le test.
Winston Ewert
@MatthewFlynn, juste pour clarifier, est-ce lié à l'exemple montré, ou autre chose à considérer pour d'autres tests unitaires? Je pouvais voir que cela importait pour quelque chose comme target.Dispose(); Assert.IsTrue(target.IsDisposed);(un exemple très simple.)
Hand-E-Food
Même dans ce cas, la propriété IsDisposed est (ou devrait être) une partie indispensable de l'interface publique de la classe, et non un détail d'implémentation. (L'interface IDispose ne fournit pas une telle propriété, mais c'est malheureux.)
Mike Nakis
2

Il y a deux aspects dans la discussion:

1. Utilisation de la cible elle-même pour le cas de test
La première question est: devez-vous / pouvez-vous utiliser la classe elle-même pour compter et obtenir une partie du travail dans le talon de test? - La réponse est NON car, en général, vous ne devez jamais faire d'hypothèse sur le code que vous testez. Si cela n'est pas fait correctement, au fil du temps, les bogues deviennent immunisés contre certains tests unitaires.

2. Le codage en dur
doit-il être codé en dur ? Encore une fois, la réponse est non . parce que comme tout logiciel - le codage en dur des informations devient difficile lorsque les choses évoluent. Par exemple, lorsque vous souhaitez que le chemin ci-dessus soit à nouveau modifié, vous devez soit écrire une unité supplémentaire, soit continuer à modifier. Une meilleure méthode consiste à conserver la date d'entrée et d'évaluation dérivée de la configuration séparée qui peut être facilement adaptée.

par exemple, voici comment je corrigerais le talon de test.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Dipan Mehta
la source
0

Il y a beaucoup de concepts possibles, fait quelques exemples pour voir la différence

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Pour résumer: en général, votre premier test juste codé en dur a plus de sens pour moi car il est simple, direct, etc. Si vous commencez à coder en dur un chemin trop de fois, mettez-le simplement dans la méthode de configuration.

Pour plus de tests structurés futurs, j'irais vérifier les sources de données afin que vous puissiez simplement ajouter plus de lignes de données si vous avez besoin de plus de situations de test.

Luc Franken
la source
0

Les frameworks de test modernes vous permettent de fournir des paramètres à votre méthode. Je les exploiterais:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

À mon avis, cela présente plusieurs avantages:

  1. Les développeurs sont souvent tentés de copier les parties apparemment simples du code de leur SUT dans leurs tests unitaires. Comme le fait remarquer Winston , ceux-ci peuvent encore contenir des bogues difficiles. Le "codage en dur" du résultat attendu permet d'éviter les situations où votre code de test est incorrect pour la même raison que votre code d'origine est incorrect. Mais si un changement dans les exigences vous oblige à retrouver des chaînes codées en dur intégrées à des dizaines de méthodes de test, cela peut être ennuyeux. Avoir toutes les valeurs codées en dur en un seul endroit, en dehors de votre logique de test, vous donne le meilleur des deux mondes.
  2. Vous pouvez ajouter des tests pour différentes entrées et sorties attendues avec une seule ligne de code. Cela vous encourage à écrire plus de tests, tout en gardant votre code de test SEC et facile à entretenir. Je trouve que parce qu'il est si bon marché d'ajouter des tests, mon esprit est ouvert à de nouveaux cas de test auxquels je n'aurais pas pensé si j'avais dû écrire une toute nouvelle méthode pour eux. Par exemple, à quel comportement puis-je m'attendre si l'une des entrées contient un point? Une barre oblique inverse? Et si on était vide? Ou un espace? Ou commencé ou terminé avec des espaces?
  3. Le cadre de test traitera chaque TestCase comme son propre test, même en mettant les entrées et sorties fournies dans le nom du test. Si tous les TestCases réussissent mais un seul, il est très facile de voir lequel s'est cassé et comment il était différent de tous les autres.
StriplingWarrior
la source