En supposant que j'ai la structure de classe (trop simplifiée) suivante:
class Base
{
public:
Base(int valueForFoo) : foo(valueForFoo) { };
virtual ~Base() = 0;
int doThings() { return foo; };
int doOtherThings() { return 42; };
protected:
int foo;
}
class BarDerived : public Base
{
public:
BarDerived() : Base(12) { };
~BarDerived() { };
int doBarThings() { return foo + 1; };
}
class BazDerived : public Base
{
public:
BazDerived() : Base(25) { };
~BazDerived() { };
int doBazThings() { return 2 * foo; };
}
Comme vous pouvez le voir, la fonction doThings
dans la classe de base renvoie des résultats différents dans chaque classe dérivée en raison des différentes valeurs de foo
, tandis que la fonction doOtherThings
se comporte de manière identique dans toutes les classes.
Lorsque je souhaite implémenter des tests unitaires pour ces classes, la gestion de doThings
, doBarThings
/doBazThings
est claire pour moi - ils doivent être couverts pour chaque classe dérivée. Mais comment gérer doOtherThings
? Est-ce une bonne pratique de dupliquer essentiellement le cas de test dans les deux classes dérivées? Le problème s'aggrave s'il existe une demi-douzaine de fonctions comme doOtherThings
, et plus classes dérivées.
Dans vos tests pour BarDerived
, vous voulez prouver que toutes les méthodes (publiques) de BarDerived
fonctionnent correctement (pour les situations que vous avez testées). De même pour BazDerived
.
Le fait que certaines des méthodes soient implémentées dans une classe de base ne change pas cet objectif de test pour BarDerived
et BazDerived
. Cela conduit à la conclusion que Base::doOtherThings
doit être testé à la fois dans le contexte de BarDerived
et BazDerived
et que vous obtenez des tests très similaires pour cette fonction.
L'avantage de tester doOtherThings
pour chaque classe dérivée est que si les exigences pour BarDerived
changent de telle sorte que BarDerived::doOtherThings
doit renvoyer 24, puis l'échec du test dans le cas de test BazDerived
vous indique que vous ne respectez peut-être pas les exigences d'une autre classe.
Mais comment les autres devraient-ils être traités? Est-ce une bonne pratique de dupliquer essentiellement le cas de test dans les deux classes dérivées?
Je m'attendrais normalement à ce que Base ait sa propre spécification, que vous pouvez vérifier pour toute implémentation conforme, y compris les classes dérivées.
void verifyBaseCompliance(const Base & systemUnderTest) {
// checks that systemUnderTest conforms to the Base API
// specification
}
void testBase () { verifyBaseCompliance(new Base()); }
void testBar () { verifyBaseCompliance(new BarDerived()); }
void testBaz () { verifyBaseCompliance(new BazDerived()); }
Vous avez un conflit ici.
Vous voulez tester la valeur de retour de doThings()
, qui repose sur un littéral (valeur const).
Tout test que vous écrivez pour cela va par nature se résumer tester une valeur const, ce qui est absurde.
Pour vous montrer un exemple plus sensé (je suis plus rapide avec C #, mais le principe est le même)
public class TriplesYourInput : Base
{
public TriplesYourInput(int input)
{
this.foo = 3 * input;
}
}
Cette classe peut être testée de manière significative:
var inputValue = 123;
var expectedOutputValue = inputValue * 3;
var receivedOutputValue = new TriplesYourInput(inputValue).doThings();
Assert.AreEqual(receivedOutputValue, expectedOutputValue);
Cela a plus de sens à tester. Sa sortie est basée sur l'entrée que vous avez choisie pour lui donner. Dans un tel cas, vous pouvez donner à une classe une entrée choisie arbitrairement, observer sa sortie et tester si elle correspond à vos attentes.
Quelques exemples de ce principe de test. Notez que mes exemples ont toujours un contrôle direct sur l'entrée de la méthode testable.
- Testez si
GetFirstLetterOfString()
retourne "F" lorsque j'entre "Flater".- Testez si
CountLettersInString()
renvoie 6 lorsque j'entre "Flater".- Testez si
ParseStringThatBeginsWithAnA()
retourne une exception lorsque j'entre "Flater".
Tous ces tests peuvent entrer quelle que soit la valeur qu'ils souhaitent , tant que leurs attentes sont conformes à ce qu'ils saisissent.
Mais si votre sortie est décidée par une valeur constante, vous devrez créer une attente constante, puis tester si la première correspond à la seconde. Ce qui est idiot, c'est soit toujours ou jamais va passer; ni l'un ni l'autre n'est un résultat significatif.
Quelques exemples de ce principe de test. Notez que ces exemples n'ont aucun contrôle sur au moins une des valeurs qui sont comparées.
- Testez si
Math.Pi == 3.1415...
- Testez si
MyApplication.ThisConstValue == 123
Ces tests pour un valeur particulière. Si vous modifiez cette valeur, vos tests échoueront. En substance, vous ne testez pas si votre logique fonctionne pour une entrée valide, vous testez simplement si quelqu'un est capable de - prédire avec précision un résultat sur lequel il n'a aucun contrôle.
Il s'agit essentiellement de tester les connaissances de l'auteur de tests sur la logique métier. Il ne s'agit pas de tester le code, mais l'auteur lui-même.
Revenant à votre exemple:
class BarDerived : public Base
{
public:
BarDerived() : Base(12) { };
~BarDerived() { };
int doBarThings() { return foo + 1; };
}
Pourquoi BarDerived
a-t-il toujours un foo
égal à 12
? Qu'est-ce que cela veut dire?
Et étant donné que vous l'avez déjà décidé, qu'essayez-vous de gagner en écrivant un test qui confirme que BarDerived
a toujours un foo
égal à 12
?
Cela devient encore pire si vous commencez à prendre en compte le fait que doThings()
peut être remplacé dans une classe dérivée. Imaginez si AnotherDerived
devait remplacer doThings()
pour qu'il renvoie toujours foo * 2
. Maintenant, vous allez avoir une classe qui est codée en dur comme Base(12)
, dont la valeur doThings()
est 24. Bien que techniquement testable, elle est dépourvue de toute signification contextuelle. Le test n'est pas compréhensible.
Je ne peux vraiment pas penser à une raison d'utiliser cette approche de valeur codée en dur. Même s'il existe un cas d'utilisation valide, je ne comprends pas pourquoi vous essayez d'écrire un test pour confirmer cette valeur codée en dur. Il n'y a rien à gagner à tester si une valeur constante est égale à la même valeur constante.
Tout échec de test prouve intrinsèquement que le test est faux. Il n'y a aucun résultat où un échec de test prouve que la logique métier est erronée. Vous n'êtes effectivement pas en mesure de confirmer ce que les tests sont créés pour confirmer en premier lieu.
La question n'a rien à voir avec l'héritage, au cas où vous vous poseriez la question. Il vous arrive d'avoir utilisé une valeur const dans le constructeur de la classe de base, mais vous auriez pu utiliser cette valeur const ailleurs et cela ne être lié à une classe héritée.
Modifier
Il y a des cas où les valeurs codées en dur ne sont pas un problème. (encore une fois, désolé pour la syntaxe C # mais le principe est toujours le même)
public class Base
{
public int MultiplyFactor;
protected int InitialValue;
public Base(int value, int factor)
{
this.InitialValue = value;
this.MultiplyFactor= factor;
}
public int GetMultipliedValue()
{
return this.InitialValue * this.MultiplyFactor;
}
}
public class DoublesYourNumber : Base
{
public DoublesYourNumber(int value) : base(value, 2) {}
}
public class TriplesYourNumber : Base
{
public TriplesYourNumber(int value) : base(value, 3) {}
}
Alors que la valeur constante (2
/3
) Influence toujours la valeur de sortie de GetMultipliedValue()
, le consommateur de votre classe a toujours contrôle aussi!
Dans cet exemple, des tests significatifs peuvent encore être écrits:
var inputValue = 123;
var expectedDoubledOutputValue = inputValue * 2;
var receivedDoubledOutputValue = new DoublesYourNumber(inputValue).GetMultipliedValue();
Assert.AreEqual(expectedDoubledOutputValue , receivedDoubledOutputValue);
var expectedTripledOutputValue = inputValue * 3;
var receivedTripledOutputValue = new TriplesYourNumber(inputValue).GetMultipliedValue();
Assert.AreEqual(expectedTripledOutputValue , receivedTripledOutputValue);
base(value, 2)
correspond à la constante dans inputValue * 2
.Le premier point n'est pas pertinent à tester. Le deuxième est!