Meilleure pratique pour les tests de débogage lors des tests unitaires

L’utilisation intensive des tests unitaires décourage-t-elle l’utilisation des assertions de débogage? Il semble que le déclenchement d’un code debug dans le code sous test implique que le test d’unité ne devrait pas exister ou que l’assertion de débogage ne devrait pas exister. “Il ne peut y en avoir qu’un” semble être un principe raisonnable. Est-ce la pratique courante? Ou est-ce que vous désactivez vos assertions de débogage lors des tests unitaires, afin qu’elles puissent être disponibles pour les tests d’intégration?

Edit: J’ai mis à jour ‘Assert’ pour déboguer l’assertion afin de distinguer une assertion dans le code sous test des lignes dans le test unitaire qui vérifie l’état après l’exécution du test.

En outre, voici un exemple qui, à mon avis, montre le dilema: Un test unitaire passe des entrées non valides pour une fonction protégée qui affirme que ses entrées sont valides. Le test unitaire ne devrait-il pas exister? Ce n’est pas une fonction publique. Peut-être que vérifier les entrées tuerait perf? Ou bien l’assertion ne devrait-elle pas exister? La fonction n’est pas protégée, elle doit donc vérifier ses entrées pour la sécurité.

C’est une question parfaitement valide.

Tout d’abord, beaucoup de gens suggèrent que vous utilisez des assertions à tort. Je pense que de nombreux experts en débogage seraient en désaccord. Bien qu’il soit recommandé de vérifier les invariants avec des assertions, les assertions ne doivent pas être limitées aux invariants d’état. En fait, de nombreux débogueurs experts vous demanderont d’affirmer toute condition susceptible de provoquer une exception en plus de vérifier les invariants.

Par exemple, considérez le code suivant:

 if (param1 == null) throw new ArgumentNullException("param1"); 

C’est très bien. Mais lorsque l’exception est levée, la stack est déroulée jusqu’à ce que quelque chose gère l’exception (probablement un gestionnaire par défaut de premier niveau). Si l’exécution s’interrompt à ce stade (vous pouvez avoir une boîte de dialog d’exception modale dans une application Windows), vous avez la possibilité de joindre un débogueur, mais vous avez probablement perdu beaucoup d’informations qui auraient pu vous aider à résoudre le problème. la majeure partie de la stack a été déroulée.

Considérons maintenant ce qui suit:

 if (param1 == null) { Debug.Fail("param1 == null"); throw new ArgumentNullException("param1"); } 

Maintenant, si le problème survient, la boîte de dialog d’assertion modale apparaît. L’exécution est interrompue instantanément. Vous êtes libre d’attacher le débogueur que vous avez choisi et d’examiner avec précision ce qui se trouve sur la stack et l’état du système au sharepoint défaillance exact. Dans une version de publication, vous obtenez toujours une exception.

Maintenant, comment traitons-nous vos tests unitaires?

Considérez un test unitaire qui teste le code ci-dessus qui inclut l’assertion. Vous voulez vérifier que l’exception est levée lorsque param1 est nul. Vous vous attendez à ce que cette assertion particulière échoue, mais tout autre échec d’assertion indiquerait que quelque chose ne va pas. Vous souhaitez autoriser des échecs d’assertion particuliers pour des tests particuliers.

La façon dont vous résolvez cela dépendra des langues que vous utilisez. Cependant, j’ai quelques suggestions si vous utilisez .NET (je n’ai pas encore essayé ceci, mais je le ferai à l’avenir et mettrai à jour le post):

  1. Cochez Trace.Listeners. Recherchez une instance de DefaultTraceListener et définissez AssertUiEnabled sur false. Cela arrête le dialog modal de surgir. Vous pouvez également effacer la collection d’auditeurs, mais vous n’obtiendrez aucune trace.
  2. Ecrivez votre propre TraceListener qui enregistre les assertions. Comment vous enregistrez les affirmations est à vous. L’enregistrement du message d’échec peut ne pas être suffisant, alors vous pouvez parcourir la stack pour trouver la méthode d’où provient l’assertion et l’enregistrer également.
  3. Une fois le test terminé, vérifiez que les seuls échecs d’assertion qui ont eu lieu sont ceux que vous attendiez. Si d’autres se sont produits, échouez au test.

Pour un exemple de TraceListener qui contient le code pour faire un tour de stack comme celui-là, je rechercherais SuperAssertListener de SUPERASSERT.NET et vérifierais son code. (Cela vaut également la peine d’intégrer SUPERASSERT.NET si vous êtes vraiment sérieux dans le débogage à l’aide d’assertions).

La plupart des frameworks de test unitaire prennent en charge les méthodes de configuration / suppression des tests. Vous souhaiterez peut-être append du code pour réinitialiser le programme d’écoute de trace et affirmer qu’il n’y a aucun échec d’assertion inattendu dans ces zones pour minimiser la duplication et éviter les erreurs.

METTRE À JOUR:

Voici un exemple de TraceListener pouvant être utilisé pour associer des assertions de test. Vous devez append une instance à la collection Trace.Listeners. Vous voudrez probablement également fournir un moyen simple pour que vos tests puissent mettre la main sur l’auditeur.

NOTE: Cela doit énormément à SUPERASSERT.NET de John Robbins.

 ///  /// TraceListener used for trapping assertion failures during unit tests. ///  public class DebugAssertUnitTestTraceListener : DefaultTraceListener { ///  /// Defines an assertion by the method it failed in and the messages it /// provided. ///  public class Assertion { ///  /// Gets the message provided by the assertion. ///  public Ssortingng Message { get; private set; } ///  /// Gets the detailed message provided by the assertion. ///  public Ssortingng DetailedMessage { get; private set; } ///  /// Gets the name of the method the assertion failed in. ///  public Ssortingng MethodName { get; private set; } ///  /// Creates a new Assertion definition. ///  ///  ///  ///  public Assertion(Ssortingng message, Ssortingng detailedMessage, Ssortingng methodName) { if (methodName == null) { throw new ArgumentNullException("methodName"); } Message = message; DetailedMessage = detailedMessage; MethodName = methodName; } ///  /// Gets a ssortingng representation of this instance. ///  ///  public override ssortingng ToSsortingng() { return Ssortingng.Format("Message: {0}{1}Detail: {2}{1}Method: {3}{1}", Message ?? "", Environment.NewLine, DetailedMessage ?? "", MethodName); } ///  /// Tests this object and another object for equality. ///  ///  ///  public override bool Equals(object obj) { var other = obj as Assertion; if (other == null) { return false; } return this.Message == other.Message && this.DetailedMessage == other.DetailedMessage && this.MethodName == other.MethodName; } ///  /// Gets a hash code for this instance. /// Calculated as recommended at http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx ///  ///  public override int GetHashCode() { return MethodName.GetHashCode() ^ (DetailedMessage == null ? 0 : DetailedMessage.GetHashCode()) ^ (Message == null ? 0 : Message.GetHashCode()); } } ///  /// Records the assertions that failed. ///  private readonly List assertionFailures; ///  /// Gets the assertions that failed since the last call to Clear(). ///  public ReadOnlyCollection AssertionFailures { get { return new ReadOnlyCollection(assertionFailures); } } ///  /// Gets the assertions that are allowed to fail. ///  public List AllowedFailures { get; private set; } ///  /// Creates a new instance of this trace listener with the default name /// DebugAssertUnitTestTraceListener. ///  public DebugAssertUnitTestTraceListener() : this("DebugAssertUnitTestListener") { } ///  /// Creates a new instance of this trace listener with the specified name. ///  ///  public DebugAssertUnitTestTraceListener(Ssortingng name) : base() { AssertUiEnabled = false; Name = name; AllowedFailures = new List(); assertionFailures = new List(); } ///  /// Records assertion failures. ///  ///  ///  public override void Fail(ssortingng message, ssortingng detailMessage) { var failure = new Assertion(message, detailMessage, GetAssertionMethodName()); if (!AllowedFailures.Contains(failure)) { assertionFailures.Add(failure); } } ///  /// Records assertion failures. ///  ///  public override void Fail(ssortingng message) { Fail(message, null); } ///  /// Gets rid of any assertions that have been recorded. ///  public void ClearAssertions() { assertionFailures.Clear(); } ///  /// Gets the full name of the method that causes the assertion failure. /// /// Credit goes to John Robbins of Wintellect for the code in this method, /// which was taken from his excellent SuperAssertTraceListener. ///  ///  private Ssortingng GetAssertionMethodName() { StackTrace stk = new StackTrace(); int i = 0; for (; i < stk.FrameCount; i++) { StackFrame frame = stk.GetFrame(i); MethodBase method = frame.GetMethod(); if (null != method) { if(method.ReflectedType.ToString().Equals("System.Diagnostics.Debug")) { if (method.Name.Equals("Assert") || method.Name.Equals("Fail")) { i++; break; } } } } // Now walk the stack but only get the real parts. stk = new StackTrace(i, true); // Get the fully qualified name of the method that made the assertion. StackFrame hitFrame = stk.GetFrame(0); StringBuilder sbKey = new StringBuilder(); sbKey.AppendFormat("{0}.{1}", hitFrame.GetMethod().ReflectedType.FullName, hitFrame.GetMethod().Name); return sbKey.ToString(); } } 

Vous pouvez append des assertions à la collection AllowedFailures au début de chaque test pour les assertions attendues.

À la fin de chaque test (j'espère que votre infrastructure de test unitaire prend en charge une méthode de déassembly de test):

 if (DebugAssertListener.AssertionFailures.Count > 0) { // TODO: Create a message for the failure. DebugAssertListener.ClearAssertions(); DebugAssertListener.AllowedFailures.Clear(); // TODO: Fail the test using the message created above. } 

IMHO debug.asserts rock. Cet excellent article montre comment les empêcher d’interrompre votre test unitaire en ajoutant un fichier app.config à votre projet de test unitaire et en désactivant la boîte de dialog:

      

Les assertions dans votre code sont (devraient être) des déclarations au lecteur qui disent “cette condition devrait toujours être vraie à ce stade”. Fait avec une certaine discipline, ils peuvent faire en sorte que le code soit correct; la plupart des gens les utilisent comme des instructions d’impression de débogage. Les tests unitaires sont des codes qui démontrent que votre code exécute correctement un scénario de test particulier. Ne faites pas bien, ils peuvent à la fois documenter les exigences et vous assurer que le code est correct.

Faites la différence Les assertions du programme vous aident à le corriger, les tests unitaires vous aident à développer la confiance de quelqu’un que le code est correct.

Comme d’autres l’ont mentionné, les assertions Debug sont destinées à des choses qui devraient toujours être vraies . (Le terme fantaisie pour cela est invariant ).

Si votre test unitaire transmet de fausses données qui déclenchent l’assertion, alors vous devez vous poser la question: pourquoi cela se produit-il?

  • Si la fonction testée est supposée traiter les données fausses, alors clairement que l’affirmation ne devrait pas être là.
  • Si la fonction n’est pas équipée pour gérer ce type de données (comme indiqué par l’assertion), alors pourquoi faites-vous des tests unitaires?

Le second point est celui auquel bon nombre de développeurs semblent s’atteler. Unité teste le diable en dehors de toutes les choses que votre code est conçu pour gérer, et Assert ou jette des exceptions pour tout le rest – Après tout, si votre code n’est pas construit pour gérer ces situations, et que vous les faites, que faire? vous vous attendez à arriver?
Vous connaissez les parties de la documentation C / C ++ qui parlent de “comportement indéfini”? Ça y est. Bail et caution


Mise à jour pour clarifier: Le revers de la médaille est que vous finissez par réaliser que vous ne devez utiliser Debug.Assert pour des choses internes appelant d’autres choses internes. Si votre code est exposé à des tiers (c’est-à-dire une bibliothèque ou autre), il n’y a pas de limite à ce que vous pouvez espérer, et vous devez donc valider correctement et lancer des exceptions ou autres, et vous devriez également tester

Une bonne configuration de test unitaire aura la capacité d’attraper des assertions. Si une assertion est déclenchée, le test en cours doit échouer et la suivante est exécutée.

Dans nos bibliothèques, des fonctionnalités de débogage de bas niveau telles que TTY / ASSERTS ont des gestionnaires appelés. Le gestionnaire par défaut imprimera / break, mais le code client peut installer des gestionnaires personnalisés pour différents comportements.

Notre structure UnitTest installe ses propres gestionnaires qui enregistrent les messages et émettent des exceptions sur les assertions. Le code UnitTest intercepte alors ces exceptions si elles se produisent et les connecte en tant qu’échec, avec l’instruction assertée.

Vous pouvez également inclure des tests d’affirmation dans votre test unitaire, par exemple

CHECK_ASSERT (someList.getAt (someList.size () + 1); // le test réussit si une assertion se produit

Voulez-vous dire les assertions C ++ / Java pour les assertions “programmation par contrat”, ou les assertions CppUnit / JUnit? Cette dernière question m’amène à croire que c’est la première.

Question intéressante, car d’après ce que je comprends, ces assertions sont souvent désactivées lors de l’exécution lors du déploiement en production. (Kinda défait le but, mais c’est une autre question.)

Je dirais qu’ils devraient être laissés dans votre code lorsque vous le testez. Vous écrivez des tests pour vous assurer que les conditions préalables sont correctement appliquées. Le test devrait être une “boîte noire”; vous devriez agir en tant que client pour la classe lorsque vous testez. Si vous les désactivez en production, cela n’invalide pas les tests.

Tout d’abord, les assertions de conception par contrat et les tests unitaires doivent être pris en compte par votre structure de test unitaire. Si vos tests unitaires échouent à cause d’un abandon de DbC, vous ne pouvez tout simplement pas les exécuter. L’alternative consiste à désactiver ces assertions lors de l’exécution (lecture compilée) de vos tests unitaires.

Puisque vous testez des fonctions non publiques, quel est le risque d’avoir une fonction invoquée avec un argument non valide? Vos tests unitaires ne couvrent-ils pas ce risque? Si vous écrivez votre code en suivant la technique TDD (Test-Driven Development), ils devraient le faire.

Si vous voulez vraiment / besoin de ces assertions de type Dbc dans votre code, vous pouvez alors supprimer les tests unitaires qui transmettent les arguments non valides aux méthodes ayant ces assertions.

Cependant, les assertions de type Dbc peuvent être utiles dans les fonctions de niveau inférieur (qui ne sont pas directement appelées par les tests unitaires) lorsque vous effectuez des tests unitaires à grain grossier.

Vous devriez garder vos assertions de débogage, même avec les tests unitaires en place.

Le problème ici n’est pas de différencier les erreurs et les problèmes.

Si une fonction vérifie ses arguments qui sont erronés, cela ne devrait pas entraîner une assertion de débogage. Au lieu de cela, il devrait retourner une valeur d’erreur. C’était une erreur d’appeler la fonction avec des parameters incorrects.

Si une fonction reçoit des données correctes, mais ne peut pas fonctionner correctement en raison du manque de mémoire, le code doit alors émettre un message de débogage en raison de ce problème. C’est un exemple d’hypothèses fondamentales qui, si elles ne sont pas respectées, “tous les paris sont désactivés”, vous devez donc vous arrêter.

Dans votre cas, écrivez le test unitaire qui fournit des valeurs erronées comme arguments. Il devrait s’attendre à une valeur de retour d’erreur (ou similaire). Obtenir un assert? – refactorez le code pour produire une erreur à la place.

Notez qu’un problème sans bogue peut toujours déclencher des assertions. Par exemple, le matériel pourrait se briser. Dans votre question, vous avez mentionné les tests d’intégration; en effet, affirmer contre des systèmes intégrés mal composés est un territoire d’affirmation; Par exemple, version de bibliothèque incompatible chargée.

Notez que la raison du “débogage” est un compromis entre être diligent / sûr et être rapide / petit.

Comme les autres l’ont mentionné, les instructions Debug.Assert doivent toujours être vraies , même si les arguments sont incorrects, l’assertion doit être vraie pour empêcher l’application d’entrer dans un état invalide, etc.

 Debug.Assert(_counter == somethingElse, "Erk! Out of wack!"); 

Vous ne devriez pas être en mesure de tester cela (et vous ne voudrez probablement pas le faire car vous ne pouvez rien faire vraiment!)

Je pourrais être loin, mais j’ai l’impression que les assertions dont vous parlez sont peut-être mieux adaptées en tant qu ‘”exceptions d’argument”, par exemple

 if (param1 == null) throw new ArgumentNullException("param1", "message to user") 

Ce type d’affirmation dans votre code est encore très testable.

PK 🙂

Cela fait un moment que cette question a été posée, mais je pense avoir une façon différente de vérifier les appels Debug.Assert () depuis un test unitaire en utilisant le code C #. Notez le bloc #if DEBUG ... #endif , qui est nécessaire pour ignorer le test lorsqu’il n’est pas exécuté dans la configuration de débogage (auquel cas Debug.Assert () ne sera pas déclenché de toute façon).

 [TestClass] [ExcludeFromCodeCoverage] public class Test { #region Variables | private UnitTestTraceListener _traceListener; private TraceListenerCollection _originalTraceListeners; #endregion #region TestInitialize | [TestInitialize] public void TestInitialize() { // Save and clear original trace listeners, add custom unit test trace listener. _traceListener = new UnitTestTraceListener(); _originalTraceListeners = Trace.Listeners; Trace.Listeners.Clear(); Trace.Listeners.Add(_traceListener); // ... Further test setup } #endregion #region TestCleanup | [TestCleanup] public void TestCleanup() { Trace.Listeners.Clear(); Trace.Listeners.AddRange(_originalTraceListeners); } #endregion [TestMethod] public void TheTestItself() { // Arrange // ... // Act // ... Debug.Assert(false, "Assert failed"); // Assert #if DEBUG // NOTE This syntax comes with using the FluentAssertions NuGet package. _traceListener.GetWriteLines().Should().HaveCount(1).And.Contain("Fail: Assert failed"); #endif } } 

La classe UnitTestTraceListener ressemble à ceci:

 [ExcludeFromCodeCoverage] public class UnitTestTraceListener : TraceListener { private readonly List _writes = new List(); private readonly List _writeLines = new List(); // Override methods public override void Write(ssortingng message) { _writes.Add(message); } public override void WriteLine(ssortingng message) { _writeLines.Add(message); } // Public methods public IEnumerable GetWrites() { return _writes.AsReadOnly(); } public IEnumerable GetWriteLines() { return _writeLines.AsReadOnly(); } public void Clear() { _writes.Clear(); _writeLines.Clear(); } } 

L’utilisation intensive des tests unitaires décourage-t-elle l’utilisation des assertions de débogage?

Non. Le contraire. Les tests unitaires rendent les assertions Debug beaucoup plus utiles en vérifiant l’état interne lors de l’exécution des tests de la boîte blanche que vous avez écrits. L’activation de Debug.Assert lors du test unitaire est essentielle, car vous envoyez rarement du code compatible avec DEBUG (sauf si les performances ne sont pas importantes du tout). Les deux seules fois où le code DEBUG est exécuté, c’est lorsque vous êtes soit 1) en train de faire ce petit test d’intégration que vous le faites vraiment, toutes les bonnes intentions et 2) d’exécuter des tests unitaires.

Il est facile d’instrumenter du code avec les tests Debug.Assert pour vérifier les invariants pendant que vous l’écrivez. Ces vérifications servent de vérifications d’aptitude lorsque les tests unitaires sont exécutés.

L’autre chose qu’Assert fait est de pointer exactement sur le premier point du code où les choses ont mal tourné. Cela peut réduire considérablement le temps de débogage lorsque votre test unitaire détecte le problème.

Cela augmente la valeur des tests unitaires.

Il semble que le déclenchement d’un code debug dans le code sous test implique que le test d’unité ne devrait pas exister ou que l’assertion de débogage ne devrait pas exister.

Cas d’espèce Cette question concerne une chose réelle qui se produit. Droite? Par conséquent, vous avez besoin d’affirmations de débogage dans votre code, et vous devez les déclencher lors des tests unitaires. La possibilité qu’un test de débogage puisse se déclencher lors d’un test unitaire démontre clairement que les assertions de débogage doivent être activées lors des tests unitaires.

Un assert firing signifie que vos tests utilisent incorrectement votre code interne (et doivent être corrigés) ou qu’une partie du code sous test appelle un autre code interne incorrect ou qu’une hypothèse fondamentale est erronée. Vous n’écrivez pas de tests parce que vous pensez que vos hypothèses sont fausses, vous… en fait, vous le faites. Vous écrivez des tests car au moins certaines de vos hypothèses sont probablement fausses. La redondance est correcte dans cette situation.

“Il ne peut y en avoir qu’un” semble être un principe raisonnable. Est-ce la pratique courante? Ou est-ce que vous désactivez vos assertions de débogage lors des tests unitaires, afin qu’elles puissent être disponibles pour les tests d’intégration?

La redondance ne nuit pas à l’exécution de vos tests unitaires. Si vous avez vraiment une couverture à 100%, l’exécution pourrait être un problème. Sinon, non, je ne suis absolument pas d’accord. Il n’y a rien de mal à vérifier votre hypothèse automatiquement au milieu d’un test. C’est pratiquement la définition du “test”.

En outre, voici un exemple qui, à mon avis, montre le dilema: Un test unitaire passe des entrées non valides pour une fonction protégée qui affirme que ses entrées sont valides. Le test d’unité ne devrait-il pas exister? Ce n’est pas une fonction publique. Peut-être que vérifier les entrées tuerait perf? Ou bien l’assertion ne devrait-elle pas exister? La fonction n’est pas protégée, elle doit donc vérifier ses entrées pour la sécurité.

Généralement, le but d’un framework de test unitaire n’est pas de tester le comportement de votre code lorsque les hypothèses invariantes ont été violées. En d’autres termes, si la documentation que vous avez écrite indique “si vous transmettez null comme paramètre, les résultats ne sont pas définis”, vous n’avez pas besoin de vérifier que les résultats sont imprévisibles. Si les résultats de l’échec sont clairement définis, ils ne sont pas indéfinis, et 1) il ne devrait pas s’agir d’un Debug.Assert, 2) vous devez définir exactement quels sont les résultats et 3) tester ce résultat. Si vous avez besoin de tester la qualité de vos assertions de débogage interne, 1) l’approche d’Andrew Grant pour créer des frameworks d’assertions, un actif testable, devrait probablement être vérifiée, et 2) vous avez une couverture de test impressionnante! Et je pense que c’est en grande partie une décision personnelle basée sur les exigences du projet. Mais je pense toujours que les assertions de débogage sont essentielles et précieuses.

En d’autres termes: Debug.Assert () augmente considérablement la valeur des tests unitaires et la redondance est une fonctionnalité.