web-dev-qa-db-fra.com

Comment puis-je tester des méthodes unitaires utilisant des méthodes statiques?

Supposons que j'ai écrit une méthode d'extension dans C # pour les tableaux byte qui les encode en chaînes hexadécimales, comme suit:

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

Je pourrais tester la méthode ci-dessus en utilisant NUnit comme suit:

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Si j'utilise le Extensions.ToHex à l'intérieur de mon projet, supposons dans Foo.Do méthode comme suit:

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

Ensuite, tous les tests de Foo.Do dépendra du succès de TestToHex_Works.

L'utilisation de fonctions libres dans C++ le résultat sera le même: les tests qui testent des méthodes qui utilisent des fonctions libres dépendront du succès des tests de fonctions libres.

Comment gérer de telles situations? Puis-je en quelque sorte résoudre ces dépendances de test? Existe-t-il un meilleur moyen de tester les extraits de code ci-dessus?

8
Akira

Ensuite, tous les tests de Foo.Do dépendra du succès de TestToHex_ Works.

Oui. C'est pourquoi vous avez des tests pour TextToHex. Si ces tests réussissent, la fonction répond à la spécification définie dans ces tests. Donc Foo.Do peut l'appeler en toute sécurité et ne pas s'en inquiéter. C'est déjà couvert.

Vous pouvez ajouter une interface, faire de la méthode une méthode d'instance et l'injecter dans Foo. Ensuite, vous pourriez vous moquer de TextToHex. Mais maintenant, vous devez écrire une maquette, qui peut fonctionner différemment. Vous aurez donc besoin d'un test "d'intégration" pour rapprocher les deux pour vous assurer que les pièces fonctionnent vraiment ensemble. Qu'est-ce que cela a fait d'autre que de rendre les choses plus complexes?

L'idée que les tests unitaires devraient tester des parties de votre code indépendamment des autres parties est une erreur. L '"unité" dans un test unitaire est une unité d'exécution isolée. Si deux tests peuvent être exécutés simultanément sans s’affecter, ils s'exécutent de manière isolée, tout comme les tests unitaires. Les fonctions statiques qui sont rapides, n'ont pas de configuration complexe et n'ont pas d'effets secondaires tels que votre exemple sont donc très bien à utiliser directement dans les tests unitaires. Si vous avez du code lent, complexe à configurer ou qui a des effets secondaires, les simulations sont utiles. Ils devraient cependant être évités ailleurs.

37
David Arno

En utilisant des fonctions libres en C++, le résultat sera le même: les tests qui testent des méthodes qui utilisent des fonctions libres dépendront du succès des tests de fonctions libres.

Comment gérer de telles situations? Puis-je en quelque sorte résoudre ces dépendances de test? Existe-t-il un meilleur moyen de tester les extraits de code ci-dessus?

Eh bien, je ne vois pas de dépendance ici. Du moins pas du genre qui nous oblige à exécuter un test avant un autre. La dépendance que nous construisons parmi les tests (peu importe le type) est de un confiance.

Nous construisons un morceau de code (test en premier ou pas) et nous nous assurons que les tests réussissent. Ensuite, nous sommes en mesure de construire plus de code. Tout le code construit sur cette première repose sur la confiance et la certitude. C'est plus ou moins ce que @DavidArno explique (très bien) dans sa réponse.

Oui. C'est pourquoi vous avez des tests pour X. Si ces tests réussissent, la fonction répond à la spécification définie dans ces tests. Ainsi, Y peut l'appeler en toute sécurité et ne pas s'en inquiéter. C'est déjà couvert.

Les tests unitaires doivent être exécutés dans n'importe quel ordre, à tout moment, dans n'importe quel environnement et aussi rapidement que possible. Qu'il s'agisse TestToHex_Works est exécuté le premier ou le dernier ne devrait pas vous inquiéter.

Si TestToHex_Works échoue en raison d'erreurs dans ToHex, tous les tests reposant sur ToHex se termineront avec des résultats différents et échoueront (idéalement). La clé ici est de détecter ces différents résultats. Nous faisons en sorte que les tests unitaires soient déterministes. Comme vous le faites ici

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

D'autres tests unitaires reposant sur ToHex devraient également être déterministes. Si tout se passe bien dans ToHex, le résultat devrait être celui attendu. Si vous en obtenez un autre, quelque chose s'est mal passé, quelque part et c'est ce que vous voulez d'un test unitaire, pour détecter ces changements subtils et échouer rapidement.

2
Laiv

C'est un peu délicat avec un exemple aussi minimal, mais revenons sur ce que nous faisons:

Supposons que j'ai écrit une méthode d'extension en c # pour les tableaux d'octets qui les code en chaînes hexadécimales, comme suit:

Pourquoi avez-vous écrit une méthode d'extension pour ce faire? À moins que vous n'écriviez une bibliothèque pour coder des tableaux d'octets en chaînes, ce n'est probablement pas la condition que vous essayez de remplir. Il semble que vous essayiez de répondre à l'exigence "Valider une charge utile qui est un tableau d'octets" - donc les tests unitaires que vous implémentez devraient être "Étant donné une charge utile X valide, ma méthode renvoie vrai" et "Étant donné une charge utile invalide Y, ma méthode renvoie false ".

Ainsi, lorsque vous implémentez initialement cela, vous pouvez faire le truc "octet à hex" en ligne dans la méthode. Et ça va. Plus tard, vous obtenez une autre exigence (par exemple, "Afficher la charge utile sous forme de chaîne hexadécimale") qui, pendant que vous l'implémentez, vous vous rendez également compte que vous devez convertir un tableau d'octets en chaîne hexadécimale. À ce stade, vous créez votre méthode d'extension, refactorisez l'ancienne méthode et appelez-la à partir de votre nouveau code. Vos tests pour la fonctionnalité de validation ne devraient pas changer. Vos tests pour afficher la charge utile doivent être les mêmes, que le code octets-hex soit en ligne ou dans une méthode statique. La méthode statique est un détail d'implémentation dont vous ne devriez pas vous soucier lors de l'écriture des tests. Le code de la méthode statique sera testé par ses consommateurs. Je ne prendrais même pas la peine de passer des tests pour ça.

1
Chris Cooper

Exactement. Et c'est l'un des problèmes des méthodes statiques, un autre étant que OOP est une bien meilleure alternative dans la plupart des situations.

C'est également l'une des raisons pour lesquelles l'injection de dépendance est utilisée.

Dans votre cas, vous pouvez préférer avoir un convertisseur concret que vous injectez dans une classe qui a besoin d'une conversion de certaines valeurs en hexadécimal. Une interface, implémentée par cette classe concrète, définirait le contrat requis pour convertir les valeurs.

Non seulement vous pourrez tester votre code plus facilement, mais vous pourrez également échanger des implémentations plus tard (car il existe de nombreuses autres façons possibles de convertir des valeurs en hexadécimal, certaines produisant différentes sorties).

0
Arseni Mourzenko