Comment écrivez-vous les cas de tests unitaires?

14

Parfois, je finis par écrire des cas de tests unitaires pour le code que d'autres développeurs ont écrit. Il y a des occasions où je ne sais vraiment pas ce que le développeur essaie de faire (la partie commerciale) et je manipule simplement le scénario de test pour obtenir la ligne verte. Ces choses sont-elles normales dans l'industrie?

Quelle est la tendance normale? Les développeurs sont-ils censés écrire des cas de tests unitaires pour le code qu'ils ont écrit eux-mêmes?

Vinoth Kumar CM
la source
2
"dint"? Que signifie "dint"?
S.Lott

Réponses:

12

Essayez de lire cet article de blog: Écrire de grands tests unitaires: meilleures et pires pratiques .

Mais il existe d'innombrables autres sur le Web.

En réponse directe à vos questions ...

  1. "Tendance normale" - je suppose que cela pourrait différer d'un endroit à l'autre, ce qui est normal pour moi peut être étrange pour les autres.
  2. Je dirais (dans mon option) que le développeur qui écrit le code devrait écrire le test, idéalement en utilisant des méthodes comme TDD, où vous écririez le test avant le code. Mais d'autres peuvent avoir des méthodes et des idées différentes ici!

Et la façon dont vous avez décrit l'écriture des tests (dans votre question) est totalement fausse !!


la source
9

Cette approche rend le test unitaire sans valeur.

Vous devez faire échouer le test unitaire lorsqu'une action réelle ne fonctionne pas comme prévu. Si vous ne le faites pas comme ça, et peut-être même écrivez le test avant le code à tester, c'est comme avoir des détecteurs de fumée qui ne fonctionnent pas.


la source
8
Ce n'est pas tout à fait vrai. Ou plutôt, c'est vrai dans un monde idéal, mais hélas, souvent nous en sommes loin. Pensez à avoir du code hérité sans tests et sans spécifications, et sans personne qui pourrait vous dire de manière fiable les moindres détails, ce qu'un code spécifique est précisément censé faire ( c'est la réalité dans une grande partie des projets existants). Même dans ce cas, il peut toujours être utile d'écrire des tests unitaires pour verrouiller l'état actuel du code et pour vous assurer de ne rien casser avec une refactorisation, des corrections de bogues ou des extensions futures.
Péter Török
2
De plus, je suppose que vous vouliez dire "écrire le test après le code à tester", n'est-ce pas?
Péter Török
@ Péter, la formulation a mal tourné - vous avez bien compris. Mais, si vous décidez d'écrire des tests, ils devraient faire quelque chose pour être utiles. Il suffit de code invoquer aveuglément en disant qu'il est un test, est - à mon avis - pas de test.
ørn, si vous voulez dire que nous devons avoir des assertions significatives dans nos tests unitaires, pour vérifier que le code testé fait bien ce que nous pensons qu'il fait, je suis entièrement d'accord.
Péter Török
3

Si vous ne savez pas ce que fait une fonction, vous ne pouvez pas écrire de test unitaire pour elle. Pour tout ce que vous savez, il ne fait même pas ce qu'il est censé faire. Vous devez d'abord découvrir ce qu'il est censé faire. PUIS écrire le test.

Edward Strange
la source
3

Dans le monde réel, il est parfaitement normal d'écrire des tests unitaires pour le code de quelqu'un d'autre. Bien sûr, le développeur d'origine aurait déjà dû le faire, mais souvent vous recevez du code hérité là où cela n'a tout simplement pas été fait. Soit dit en passant, peu importe si ce code hérité est venu il y a des décennies d'une galaxie très éloignée, ou si l'un de vos collègues l'a vérifié la semaine dernière, ou si vous l'avez écrit aujourd'hui, le code hérité est un code sans test

Demandez-vous: pourquoi écrivons-nous des tests unitaires? Passer au vert n'est évidemment qu'un moyen de parvenir à une fin, le but ultime est de prouver ou d'infirmer des affirmations sur le code testé.

Imaginons que vous disposiez d'une méthode qui calcule la racine carrée d'un nombre à virgule flottante. En Java, l'interface le définirait comme:

public double squareRoot(double number);

Peu importe que vous ayez écrit l'implémentation ou que quelqu'un d'autre l'ait fait, vous voulez affirmer quelques propriétés de squareRoot:

  1. qu'il peut retourner des racines simples comme sqrt (4.0)
  2. qu'il peut trouver une vraie racine comme sqrt (2.0) avec une précision raisonnable
  3. qu'il trouve que sqrt (0,0) est 0,0
  4. qu'il lève une exception IllegalArgumentException lorsqu'il reçoit un nombre négatif, c'est-à-dire sur sqrt (-1.0)

Vous commencez donc à les écrire en tant que tests individuels:

@Test
public void canFindSimpleRoot() {
  assertEquals(2, squareRoot(4), epsilon);
}

Oups, ce test échoue déjà:

java.lang.AssertionError: Use assertEquals(expected, actual, delta) to compare floating-point numbers

Vous avez oublié l'arithmétique à virgule flottante. OK, vous présentez double epsilon=0.01et allez:

@Test
public void canFindSimpleRootToEpsilonPrecision() {
  assertEquals(2, squareRoot(4), epsilon);
}

et ajoutez les autres tests: enfin

@Test
@ExpectedException(IllegalArgumentException.class)
public void throwsExceptionOnNegativeInput() {
  assertEquals(-1, squareRoot(-1), epsilon);
}

et oups, encore une fois:

java.lang.AssertionError: expected:<-1.0> but was:<NaN>

Vous auriez dû tester:

@Test
public void returnsNaNOnNegativeInput() {
  assertEquals(Double.NaN, squareRoot(-1), epsilon);
}

Qu'avons-nous fait ici? Nous avons commencé avec quelques hypothèses sur la façon dont la méthode devrait se comporter, et nous avons constaté que toutes n'étaient pas vraies. Nous avons ensuite rendu la suite de tests verte, pour noter que la méthode se comporte selon nos hypothèses corrigées. Désormais, les clients de ce code peuvent compter sur ce comportement. Si quelqu'un devait échanger l'implémentation réelle de squareRoot avec quelque chose d'autre, quelque chose qui, par exemple, a vraiment levé une exception au lieu de renvoyer NaN, nos tests le détecteraient immédiatement.

Cet exemple est trivial, mais souvent vous héritez de gros morceaux de code où il n'est pas clair ce qu'il fait réellement. Dans ce cas, il est normal de placer un faisceau de test autour du code. Commencez avec quelques hypothèses de base sur la façon dont le code doit se comporter, écrivez des tests unitaires pour eux, testez. Si vert, bon, écrivez plus de tests. Si Rouge, eh bien maintenant vous avez une assertion ratée que vous pouvez tenir contre une spécification. Il y a peut-être un bogue dans le code hérité. Peut-être que la spécification n'est pas claire sur cette entrée particulière. Vous n'avez peut-être pas de spécification. Dans ce cas, réécrivez le test de sorte qu'il documente le comportement inattendu:

@Test
public void throwsNoExceptionOnNegativeInput() {
  assertNotNull(squareRoot(-1)); // Shouldn't this fail?
}

Au fil du temps, vous vous retrouvez avec un faisceau de test qui documente le comportement réel du code et devient une sorte de spécification codée. Si vous souhaitez modifier le code hérité ou le remplacer par autre chose, vous disposez du faisceau de test pour vérifier que le nouveau code se comporte de la même manière ou que le nouveau code se comporte différemment de manière attendue et contrôlée (par exemple, corrige le bug que vous attendez qu'il corrige). Ce harnais ne doit pas être complet le premier jour, en fait, avoir un harnais incomplet est presque toujours mieux que de ne pas avoir de harnais du tout. Avoir un harnais signifie que vous pouvez écrire votre code client avec plus de facilité, vous savez où vous attendre à ce que les choses se cassent lorsque vous changez quelque chose et où elles se cassent quand elles le font finalement.

Vous devriez essayer de sortir de la mentalité selon laquelle vous devez passer des tests unitaires simplement parce que vous le devez, comme vous rempliriez des champs obligatoires sur un formulaire. Et vous ne devez pas écrire de tests unitaires juste pour rendre la ligne rouge verte. Les tests unitaires ne sont pas vos ennemis, les tests unitaires sont vos amis.

Wallenborn
la source
1

Lorsque je rédige des cas de test (pour les imprimantes), j'essaie de penser à chaque petit composant .... et que puis-je faire pour éventuellement le casser. Disons donc le scanner par exemple, quelles commandes utilise-t-il (dans le langage de travail d'imprimante pjl) que puis-je écrire pour tester chaque fonctionnalité ... Ok maintenant, que puis-je faire pour essayer de casser cela.

J'essaie de le faire pour chaque composant majeur, mais en ce qui concerne les logiciels et non pas tant le matériel que vous voulez regarder chaque méthode / fonction et vérifier les limites et autres.


la source
1

Il semble que vous travaillez avec d'autres développeurs (ou que vous maintenez du code écrit par d'autres développeurs) qui ne font pas de tests unitaires. Dans ce cas, je pense que vous voudriez certainement savoir ce que l'objet ou la méthode que vous testez est censé faire, puis créer un test pour cela.

Ce ne sera pas TDD car vous n'avez pas écrit le test en premier, mais vous pourriez améliorer la situation. Vous pouvez également créer une copie des objets testés avec des talons afin de pouvoir établir que vos tests fonctionnent correctement lorsque le code échoue.

vjones
la source