web-dev-qa-db-fra.com

Test unitaire et vérification de la valeur des variables privées

J'écris des tests unitaires avec C #, NUnit et Rhino Mocks. Voici les parties pertinentes d'une classe que je teste:

public class ClassToBeTested
{
    private IList<object> insertItems = new List<object>();

    public bool OnSave(object entity, object id)
    {
        var auditable = entity as IAuditable;
        if (auditable != null) insertItems.Add(entity);

        return false;            
    }
}

Je veux tester les valeurs dans insertItems après un appel à OnSave:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to insertItems array            
}

Quelle est la meilleure pratique pour cela? J'ai envisagé d'ajouter insertItems en tant que propriété avec un get public ou d'injecter une liste dans ClassToBeTested, mais je ne suis pas sûr que je devrais modifier le code à des fins de test.

J'ai lu de nombreux articles sur les tests de méthodes privées et la refactorisation, mais c'est une classe tellement simple que je me suis demandé quelle était la meilleure option.

46
TonE

La réponse rapide est que vous ne devriez jamais accéder à des membres non publics à partir de vos tests unitaires. Cela défie totalement le but d'avoir une suite de tests, car il vous enferme dans des détails d'implémentation internes que vous ne voudrez peut-être pas conserver de cette façon.

La réponse la plus longue se rapporte à quoi faire alors? Dans ce cas, il est important de comprendre pourquoi l'implémentation est telle qu'elle est (c'est pourquoi TDD est si puissant, car nous utilisons les tests pour spécifier le comportement attendu, mais j'ai l'impression que vous n'utilisez pas TDD).

Dans votre cas, la première question qui vous vient à l'esprit est: "Pourquoi les objets IAuditable sont-ils ajoutés à la liste interne?" ou, autrement dit, "Quel est le résultat attendu visible de l'extérieur de cette implémentation?" Selon la réponse à ces questions, c'est ce que vous devez tester.

Si vous ajoutez les objets IAuditable à votre liste interne parce que vous souhaitez ensuite les écrire dans un journal d'audit (juste une supposition), appelez la méthode qui écrit le journal et vérifiez que les données attendues ont été écrites.

Si vous ajoutez l'objet IAuditable à votre liste interne parce que vous souhaitez rassembler des preuves contre une sorte de contrainte ultérieure, essayez de le tester.

Si vous avez ajouté le code sans raison mesurable, supprimez-le à nouveau :)

L'important est qu'il est très avantageux de tester le comportement au lieu de l'implémentation . Il s'agit également d'une forme de test plus robuste et maintenable.

N'ayez pas peur de modifier votre système sous test (SUT) pour être plus testable. Tant que vos ajouts ont un sens dans votre domaine et suivent les meilleures pratiques orientées objet, il n'y a pas de problème - vous suivriez simplement le principe ouvert/fermé .

90
Mark Seemann

Vous ne devriez pas vérifier la liste où l'élément a été ajouté. Si vous faites cela, vous écrirez un test unitaire pour la méthode Add dans la liste, et non un test pour le code votre. Vérifiez simplement la valeur de retour d'OnSave; c'est vraiment tout ce que vous voulez tester.

Si vous êtes vraiment préoccupé par l'ajout, moquez-le de l'équation.

Éditer:

@TonE: Après avoir lu vos commentaires, je dirais que vous voudrez peut-être modifier votre méthode OnSave actuelle pour vous informer des échecs. Vous pouvez choisir de lever une exception si le lancer échoue, etc. Vous pouvez alors écrire un test unitaire qui attend et une exception, et un qui ne le fait pas.

6
Esteban Araya

Je dirais que la "meilleure pratique" consiste à tester quelque chose de significatif avec l'objet qui est différent maintenant qu'il a stocké l'entité dans la liste.

En d'autres termes, quel comportement est différent de la classe maintenant qu'elle l'a stockée et testez ce comportement. Le stockage est un détail d'implémentation.

Cela étant dit, il n'est pas toujours possible de le faire.

Vous pouvez utiliser réflexion si vous le devez.

2
Yishai

Si je ne me trompe pas, ce que vous voulez vraiment tester, c'est qu'il n'ajoute des éléments à la liste que lorsqu'ils peuvent être castés en IAuditable. Donc, vous pourriez écrire quelques tests avec des noms de méthode comme:

  • NotIAuditableIsNotSaved
  • IAuditableInstanceIsSaved
  • IAuditableSubclassInstanceIsSaved

... et ainsi de suite.

Le problème est que, comme vous le constatez, étant donné le code dans votre question, vous ne pouvez le faire que par indirection - uniquement en vérifiant le insertItems IList<object> membre (par réflexion ou en ajoutant une propriété dans le seul but de tester) ou en injectant la liste dans la classe:

public class ClassToBeTested
{
    private IList _InsertItems = null;

    public ClassToBeTested(IList insertItems) {
      _InsertItems = insertItems;
    }
}

Ensuite, il est simple de tester:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     List<object> testList = new List<object>();
     myClassToBeTested     = new MyClassToBeTested(testList);

     // ... create audiableObject here, etc.
     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to testList
}

L'injection est la solution la plus prospective et la moins intrusive, sauf si vous avez des raisons de penser que la liste serait une partie précieuse de votre interface publique (auquel cas, l'ajout d'une propriété pourrait être supérieur - et bien sûr l'injection de propriété est également parfaitement légitime). Vous pouvez même conserver un constructeur sans argument qui fournit une implémentation par défaut (nouveau List ()).

C'est en effet une bonne pratique; Cela peut vous sembler un peu trop ingénieux, étant donné que c'est une classe simple, mais la testabilité en vaut la peine. Ensuite, si vous trouvez un autre endroit où vous souhaitez utiliser la classe, ce sera la cerise sur le gâteau, car vous ne serez pas limité à utiliser un IList (pas que cela prendrait beaucoup d'efforts pour faire le changement plus tard ).

1
Jeff Sternal

Si la liste est un détail d'implémentation interne (et il semble l'être), vous ne devriez pas le tester.

Une bonne question est, quel est le comportement qui serait attendu si l'élément était ajouté à la liste? Cela peut nécessiter une autre méthode pour le déclencher.

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        MyOtherClass other = new MyOtherClass();
        c.Save(other);

        var result = c.Retrieve();
        Assert.IsTrue(result.Contains(other));
    }

Dans ce cas, j'affirme que le comportement correct, visible de l'extérieur, est qu'après avoir enregistré l'objet, il sera inclus dans la collection récupérée.

Si le résultat est qu'à l'avenir, l'objet transmis devrait avoir un appel à lui dans certaines circonstances, alors vous pourriez avoir quelque chose comme ça (veuillez pardonner la pseudo-API):

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        IThing other = GetMock();
        c.Save(other);

        c.DoSomething();
        other.AssertWasCalled(o => o.SomeMethod());
    }

Dans les deux cas, vous testez le comportement visible de l'extérieur de la classe, pas l'implémentation interne.

1
kyoryu

Le nombre de tests dont vous avez besoin dépend de la complexité du code - combien de points de décision sont là, à peu près. Différents algorithmes peuvent obtenir le même résultat avec une complexité différente dans leur mise en œuvre. Comment rédigez-vous un test indépendant de la mise en œuvre et assurez-vous tout de même d'avoir une couverture adéquate de vos points de décision?

Maintenant, si vous concevez des tests plus importants, par exemple au niveau de l'intégration, alors, non, vous ne voudriez pas écrire sur l'implémentation ou tester des méthodes privées, mais la question était dirigée vers la petite portée de test unitaire.

0
Chris Golledge