Génération d’entrées à l’aide de l’exécution symbolique dynamique

IntelliTest génère des entrées pour les tests unitaires paramétrables en analysant les conditions de branche dans le programme. Les entrées de test sont choisies en fonction de leur capacité à déclencher de nouveaux comportements de création de branches du programme. L’analyse est un processus incrémentiel. Elle redéfinit un prédicat q: I -> {true, false} sur les paramètres d’entrée du test formels I. q représente l’ensemble des comportements déjà observés par IntelliTest. Au départ, q := false, car rien n’a encore été observé.

Les étapes de la boucle sont :

  1. IntelliTest détermine les entrées i de façon que q(i)=false en utilisant un solveur de contrainte. Par construction, l’entrée i prend un chemin d’exécution non emprunté auparavant. Cela signifie qu’au départ, i peut être n’importe quelle entrée, car aucun chemin d’exécution n’a encore été découvert.

  2. IntelliTest exécute le test avec l’entrée i choisie, et surveille l’exécution du test et le programme testé.

  3. Pendant l’exécution, le programme prend un chemin particulier qui est déterminé par toutes les branches conditionnelles du programme. L’ensemble de toutes les conditions qui déterminent l’exécution est appelé la condition de chemin, écrite sous la forme du prédicat p: I -> {true, false} sur les paramètres d’entrée formels. IntelliTest calcule une représentation de ce prédicat.

  4. IntelliTest définit q := (q or p). En d’autres termes, il enregistre le fait qu’il a vu le chemin représenté par p.

  5. Passez à l’étape 1.

Le solveur de contrainte d’IntelliTest peut gérer les valeurs de tous les types pouvant apparaître dans les programmes .NET :

IntelliTest filtre les entrées qui ne respectent pas les hypothèses indiquées.

En plus des entrées immédiates (arguments ou tests unitaires paramétrables), un test peut extraire d’autres valeurs d’entrée de la classe statique PexChoose. Les choix déterminent aussi le comportement des objets fictifs paramétrables.

Solveur de contrainte

IntelliTest utilise un solveur de contrainte pour déterminer les valeurs d’entrée appropriées d’un test et du programme testé.

IntelliTest utilise le solveur de contrainte Z3.

Couverture dynamique de code

Effet collatéral de la surveillance de l’exécution, IntelliTest collecte les données de couverture dynamique de code. La couverture est dite dynamique car IntelliTest connaît uniquement le code qui a été exécuté. Par conséquent, il ne peut pas donner de valeurs absolues pour la couverture comme d’autres outils de couverture peuvent généralement en donner.

Par exemple, quand IntelliTest signale la couverture dynamique de 5/10 blocs de base, cela signifie que cinq blocs sur dix ont été couverts, où le nombre total de blocs de toutes les méthodes qui ont été atteintes jusqu'à présent par l’analyse (par opposition à toutes les méthodes qui existent dans l’assembly testé) est de 10. Plus tard au cours de l’analyse, quand d’autres méthodes accessibles sont découvertes, le numérateur (5 dans cet exemple) et le dénominateur (10) peuvent tous les deux augmenter.

Nombres entiers et à virgule flottante

Le solveur de contrainte d’IntelliTest détermine les valeurs d’entrée de test des types primitifs tels que byte, int, float et d’autres, afin de déclencher des chemins d’exécution différents pour le test et le programme testé.

Objets

IntelliTest peut créer des instances de classes .NET existantes ou vous pouvez utiliser IntelliTest pour automatiquement créer des objets fictifs qui implémentent une interface spécifique et qui se comportent différemment selon leur utilisation.

Instancier les classes existantes

Quel est le problème ?

IntelliTest surveille les instructions exécutées quand il exécute un test et le programme testé. Il surveille en particulier tous les accès aux champs. Il utilise ensuite un solveur de contrainte pour déterminer les nouvelles entrées de test, notamment les objets et leurs valeurs de champ qui entraînent d’autres comportements intéressants du test et du programme testé.

Cela signifie qu’IntelliTest doit créer des objets de certains types et définir leurs valeurs de champ. Si la classe est visible et a un constructeur visible par défaut, IntelliTest peut créer une instance de la classe. Si tous les champs de la classe sont visibles, IntelliTest peut définir les champs automatiquement.

Si le type n’est pas visible ou si les champs ne sont pas visibles, IntelliTest a besoin d’aide pour créer des objets et les mettre dans des états intéressants qui permettent d’obtenir une couverture maximale du code. IntelliTest peut utiliser la réflexion pour créer et initialiser des instances de manière arbitraire. Toutefois, ce n’est généralement pas souhaitable, car cela peut placer l’objet dans un état qui ne peut jamais se produire durant l’exécution normale du programme. Au lieu de cela, IntelliTest s’appuie sur des indications de l’utilisateur.

Visibilité

.NET a un modèle de visibilité élaboré : les types, méthodes, champs et autres membres peuvent être privés, publics, internes et plus encore.

Quand IntelliTest génère des tests, il tente d’effectuer uniquement les actions (comme l’appel des constructeurs et des méthodes et la définition des champs) qui sont autorisées selon les règles de visibilité de .NET dans le contexte des tests générés.

Les règles sont les suivantes :

  • Visibilité des membres internes

    • IntelliTest part du principe que les tests générés ont accès aux membres internes qui étaient visibles par la classe PexClass englobante. .NET a l’attribut InternalsVisibleToAttribute pour étendre la visibilité des membres internes à d’autres assemblys.
  • Visibilité des membres privés et de la famille (protégés en C#) de la classe PexClass

    • IntelliTest place toujours les tests générés directement dans la classe PexClass ou dans une sous-classe. Par conséquent, IntelliTest part du principe qu’il peut utiliser tous les membres de la famille visibles (protégés en C#).
    • Si les tests générés sont placés directement dans la classe PexClass (généralement en utilisant des classes partielles), IntelliTest part du principe qu’il peut également utiliser tous les membres privés de la classe PexClass.
  • Visibilité des membres publics

    • IntelliTest part du principe qu’il peut utiliser tous les membres exportés visibles dans le contexte de la classe PexClass.

Objets fictifs paramétrés

Comment tester une méthode qui a un paramètre d’un type d’interface ? Ou d’une classe non-sealed ? IntelliTest ne sait pas quelles implémentations seront utilisées ultérieurement quand cette méthode est appelée. Peut-être même qu’il n’y a aucune véritable implémentation disponible au moment du test.

La réponse classique consiste à utiliser des objets fictifs avec un comportement explicite.

Un objet fictif implémente une interface (ou étend une classe non-sealed). Il ne représente pas une véritable implémentation, mais juste un raccourci qui permet l’exécution des tests à l’aide de l’objet fictif. Son comportement est défini manuellement dans chaque cas de test où il est utilisé. De nombreux outils permettent de définir facilement des objets fictifs et leur comportement attendu, mais ce comportement doit toujours être défini manuellement.

À la place des valeurs codées en dur dans les objets fictifs, IntelliTest peut générer les valeurs. Tout comme il autorise les tests unitaires paramétrisés, IntelliTest autorise également les objets fictifs paramétrisés.

Les objets fictifs paramétrables ont deux modes d’exécution différents :

  • sélection : lors de l’exploration du code, les objets fictifs paramétrables sont une source d’entrées de test supplémentaire à partir de laquelle IntelliTest va essayer de choisir des valeurs intéressantes.
  • réexécution : lors de l’exécution d’un test généré précédemment, les objets fictifs paramétrables se comportent comme des stubs avec un comportement (en d’autres termes, un comportement prédéfini).

Utilisez PexChoose pour obtenir les valeurs des objets fictifs paramétrables.

Structures

Le raisonnement d’IntelliTest concernant les valeurs struct est similaire à la façon dont il traite les objets.

Tableaux et chaînes

IntelliTest surveille les instructions exécutées quand il exécute un test et le programme testé. Il est particulièrement attentif quand le programme dépend de la longueur d’une chaîne ou d’un tableau (et des limites et longueurs inférieures d’un tableau multidimensionnel). Il observe aussi comment le programme utilise les différents éléments d’une chaîne ou d’un tableau. Il utilise ensuite un solveur de contrainte pour déterminer les longueurs et les valeurs d’élément pouvant entraîner des comportements intéressants du test et du programme testé.

IntelliTest tente de réduire la taille des tableaux et des chaînes nécessaires pour déclencher des comportements de programme intéressants.

Obtenir des entrées supplémentaires

La classe statique PexChoose peut être utilisée pour obtenir des entrées supplémentaires dans un test et pour implémenter des objets fictifs paramétrables.

Vous avez des commentaires ?

Postez vos idées et demandes de fonctionnalités sur la Communauté des développeurs.