53 votes

Méthode recommandée pour les assertions de débogage lors des tests unitaires

Ne une forte utilisation de tests unitaires de décourager l'utilisation de débogage affirme? Il semble comme un debug assertion de tir dans le code en cours de test implique l'unité de test ne devrait pas exister ou le debug assertion ne devrait pas exister. "Il peut y avoir un seul" semble raisonnable principe. Est-ce la pratique courante? Ou avez-vous désactiver le débogage affirme lors de tests unitaires, de sorte qu'ils peuvent être de l'ordre pour des tests d'intégration?

Edit: j'ai mis à jour 'Affirmer' debug assertion de distinguer une assertion dans le code en cours de test à partir des lignes dans l'unité de test de vérification de l'état après le test a été exécuté.

Aussi, voici un exemple qui, je crois, montre le dilemme: Un test unitaire passe non valide les intrants pour une fonction protégée qui affirme les entrées sont valides. Si l'appareil de test n'existent pas? Ce n'est pas une fonction publique. Peut-être vérifier les entrées de tuer perf? Ou doit l'affirmer n'existent pas? La fonction est protégée pas privé de sorte qu'il convient de vérifier les entrées pour la sécurité.

42voto

Alex Humphrey Points 2794

C'est une question tout à fait pertinente.

Tout d'abord, beaucoup de gens sont ce qui suggère que vous êtes à l'aide d'assertions à tort. Je pense que beaucoup de débogage experts sont en désaccord. Bien qu'il soit recommandé de vérifier les invariants avec des assertions les assertions ne devrait pas être limitée à l'état d'invariants. En fait, de nombreux experts débogueurs vous dira de faire valoir que les conditions qui peuvent provoquer une exception en plus de vérifier les invariants.

Par exemple, considérons le code suivant:

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

C'est très bien. Mais lorsque l'exception est levée, la pile devient déroulée jusqu'à ce que quelque chose gère l'exception (probablement un haut niveau de gestionnaire par défaut). Si l'exécution s'arrête à ce point (vous pouvez avoir une modale de dialogue d'exception dans une application Windows), vous avez la possibilité d'attacher un débogueur, mais vous avez probablement perdu beaucoup d'informations qui pourraient vous ont aidé à résoudre le problème, car la plupart de la pile a été dénouée.

Maintenant, considérez les points suivants:

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

Maintenant, si le problème se produit, le modal affirmer boîte de dialogue apparaît. L'exécution est suspendue instantanément. Vous êtes libre de fixer votre choix débogueur et enquêter sur exactement ce qui est sur la pile et l'état du système au point exact de la panne. Dans un communiqué de construire, vous obtenez toujours une exception.

Maintenant, comment faire nous nous occupons de vos tests unitaires?

Considérons une unité de test qui permet de tester le code ci-dessus qui comprend l'affirmation. Vous voulez vous assurer que l'exception est levée lorsque param1 est null. Vous s'attendre à ce que de revendication particulière à l'échec, mais n'importe quelle autre affirmation échecs indique que quelque chose est faux. Vous souhaitez permettre de revendication particulière des échecs pour les essais particuliers.

Pour résoudre cela dépendra de ce que les langues etc. vous êtes en utilisant. Cependant, j'ai quelques suggestions si vous êtes en utilisant .NET (je n'ai pas vraiment essayé, mais je le ferai dans l'avenir et de mettre à jour le post):

  1. Vérifier La Trace.Les auditeurs. Trouver toutes les instances de DefaultTraceListener et définir AssertUiEnabled à false. Cela empêche la boîte de dialogue modale à partir de popping up. Vous pouvez également effacer les auditeurs de la collection, mais vous n'aurez pas de traçage que ce soit.
  2. Écrivez votre propre TraceListener qui enregistre des assertions. Comment vous enregistrez des assertions est à vous. L'enregistrement du message d'erreur peut ne pas être assez bon, de sorte que vous pouvez parcourir la pile de trouver la méthode de l'affirmation venue et d'enregistrement que trop.
  3. Une fois qu'un test se termine, vérifiez que la seule affirmation des échecs qui ont eu lieu ont été ceux que vous attendiez. Si toutes les autres sont survenus, ne passent pas le test.

Pour un exemple de TraceListener qui contient le code pour faire une pile de marche comme ça, j'aurais de la recherche pour SUPERASSERT.NET's SuperAssertListener et vérifier son code. (Il est également intéressant de l'intégration SUPERASSERT.NET si vous êtes vraiment sérieux au sujet de débogage à l'aide d'assertions).

La plupart des infrastructures de test unitaire de soutien de la configuration de test/méthodes de démontage. Vous pouvez ajouter du code pour réinitialiser l'écouteur de suivi et d'affirmer qu'il n'y en a pas à tous les imprévus affirmation des échecs dans ces domaines afin de réduire la duplication et éviter les erreurs.

Mise à JOUR:

Voici un exemple TraceListener qui peut être utilisé pour l'unité de test affirmations. Vous devez ajouter une instance de la Trace.Les auditeurs de la collection. Vous voudrez aussi de fournir facilement de manière à ce que vos tests peuvent obtenir de l'auditeur.

NOTE: Ceci doit beaucoup à John Robbins SUPERASSERT.NET.

/// <summary>
/// TraceListener used for trapping assertion failures during unit tests.
/// </summary>
public class DebugAssertUnitTestTraceListener : DefaultTraceListener
{
    /// <summary>
    /// Defines an assertion by the method it failed in and the messages it
    /// provided.
    /// </summary>
    public class Assertion
    {
        /// <summary>
        /// Gets the message provided by the assertion.
        /// </summary>
        public String Message { get; private set; }

        /// <summary>
        /// Gets the detailed message provided by the assertion.
        /// </summary>
        public String DetailedMessage { get; private set; }

        /// <summary>
        /// Gets the name of the method the assertion failed in.
        /// </summary>
        public String MethodName { get; private set; }

        /// <summary>
        /// Creates a new Assertion definition.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="detailedMessage"></param>
        /// <param name="methodName"></param>
        public Assertion(String message, String detailedMessage, String methodName)
        {
            if (methodName == null)
            {
                throw new ArgumentNullException("methodName");
            }

            Message = message;
            DetailedMessage = detailedMessage;
            MethodName = methodName;
        }

        /// <summary>
        /// Gets a string representation of this instance.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format("Message: {0}{1}Detail: {2}{1}Method: {3}{1}",
                Message ?? "<No Message>",
                Environment.NewLine,
                DetailedMessage ?? "<No Detail>",
                MethodName);
        }

        /// <summary>
        /// Tests this object and another object for equality.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// Gets a hash code for this instance.
        /// Calculated as recommended at http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return
                MethodName.GetHashCode() ^
                (DetailedMessage == null ? 0 : DetailedMessage.GetHashCode()) ^
                (Message == null ? 0 : Message.GetHashCode());
        }
    }

    /// <summary>
    /// Records the assertions that failed.
    /// </summary>
    private readonly List<Assertion> assertionFailures;

    /// <summary>
    /// Gets the assertions that failed since the last call to Clear().
    /// </summary>
    public ReadOnlyCollection<Assertion> AssertionFailures { get { return new ReadOnlyCollection<Assertion>(assertionFailures); } }

    /// <summary>
    /// Gets the assertions that are allowed to fail.
    /// </summary>
    public List<Assertion> AllowedFailures { get; private set; }

    /// <summary>
    /// Creates a new instance of this trace listener with the default name
    /// DebugAssertUnitTestTraceListener.
    /// </summary>
    public DebugAssertUnitTestTraceListener() : this("DebugAssertUnitTestListener") { }

    /// <summary>
    /// Creates a new instance of this trace listener with the specified name.
    /// </summary>
    /// <param name="name"></param>
    public DebugAssertUnitTestTraceListener(String name) : base()
    {
        AssertUiEnabled = false;
        Name = name;
        AllowedFailures = new List<Assertion>();
        assertionFailures = new List<Assertion>();
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="detailMessage"></param>
    public override void Fail(string message, string detailMessage)
    {
        var failure = new Assertion(message, detailMessage, GetAssertionMethodName());

        if (!AllowedFailures.Contains(failure))
        {
            assertionFailures.Add(failure);
        }
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    public override void Fail(string message)
    {
        Fail(message, null);
    }

    /// <summary>
    /// Gets rid of any assertions that have been recorded.
    /// </summary>
    public void ClearAssertions()
    {
        assertionFailures.Clear();
    }

    /// <summary>
    /// 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.
    /// </summary>
    /// <returns></returns>
    private String 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 ajouter des Affirmations AllowedFailures collection au début de chaque test pour les affirmations que vous attendez.

À la fin de chaque test (j'espère que votre framework de test unitaire prend en charge un test de permutation de la méthode) faire:

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.
}

10voto

Orion Edwards Points 54939

Comme d'autres l'ont mentionné, le Débogage affirme sont destinés pour des choses qui devraient toujours être vrai. (Le terme de fantaisie pour ce qui est des invariants).

Si votre unité de test est de passage en bidon de données qui est le déclenchement de l'affirmer, alors vous devez vous poser la question - pourquoi est-ce qui se passe?

  • Si la fonction de test est censé traiter avec des données fausses, évidemment, qui affirment ne devrait pas être là.
  • Si la fonction n'est pas équipé pour faire face à ce genre de données (comme indiqué par l'assertion), alors pourquoi faire des tests unitaires pour elle?

Le deuxième point est celui qui tout à fait un peu de développeurs semblent tomber dans l'. Unité de test le diable hors de toutes les choses que votre code est intégré à traiter, et d'Affirmer ou de lancer des exceptions pour tout le reste, Après tout, si votre code n'est PAS construit pour faire face à ces situations, et vous l'origine de ce changement, qu'attendez-vous d'arriver?
Vous savez, ces parties du C/C++ documentation parler de "comportement indéfini"? Ce qu'il est. Caution et caution dur.

8voto

Charlie Martin Points 62306

Les Assertions dans votre code sont (devraient être) des déclarations pour le lecteur que de dire "cette condition doit toujours être vrai à ce point." Fait avec de la discipline, ils peuvent faire partie de veiller à ce que le code est correct, la plupart des gens les utilisent comme débogage imprimer des relevés. Les Tests unitaires sont code qui montre que votre code correctement effectue un test en particulier le cas; ne'e bien, ils peuvent à la fois document de la reuirements, et augmenter votre confiance que le code est tout à fait exact.

Obtenez la différence? Le programme des assertions de vous aider à le faire corriger, les tests unitaires pour vous aider à développer de quelqu'un d'autre de la confiance que le code est correct.

2voto

Andrew Grant Points 35305

Un bon test de l'unité d'installation aura la capacité d'attraper l'affirme. Si une assertion est déclenché le courant de test échoue et le suivant est exécuté.

Dans nos bibliothèques de bas niveau de débogage des fonctionnalités telles que l'ATS/AFFIRME avoir des gestionnaires qui sont appelés. Le gestionnaire par défaut sera printf/pause, mais le code client peut installer des gestionnaires pour des comportements différents.

Notre UnitTest cadre installe ses propres gestionnaires de messages de journal et de lancer des exceptions sur l'affirme. Le UnitTest code va alors s'emparer de ces exceptions si elles se produisent et les enregistre comme un échec, ainsi que l'affirme la déclaration.

Vous pouvez également inclure affirmer le test dans votre unité de test par exemple

CHECK_ASSERT(someList.getAt(someList.size() + 1); // test passe si une assertion se produit

1voto

Faisal Memon Points 196

Vous devez garder votre debug affirme, même avec des tests unitaires en place.

Le problème ici n'est pas de différenciation entre les Erreurs et les Problèmes.

Si une fonction vérifie ses arguments qui sont erronées, il ne faudrait pas que l'assertion de débogage. Au lieu de cela, il doit retourner une erreur de valeur de retour. C'était une Erreur d'appeler la fonction avec des paramètres incorrects.

Si une fonction est passée de données correct, mais ne peuvent pas fonctionner correctement en raison de la durée de fonctionnement est à court de mémoire, le code de débogage affirmer à cause de ce Problème. Qu'un exemple des hypothèses fondamentales qui, s'ils ne détiennent pas, "tous les paris sont éteints", de sorte que vous devez y mettre fin.

Dans votre cas, faire écrire le test de l'unité qui fournit des valeurs erronées comme arguments. Il faut s'attendre à une erreur de valeur de retour (ou similaire). Obtenir une assertion? -- refactoriser le code pour produire une erreur à la place.

Noter un bug sans problème peut encore déclencher affirme; par exemple, le matériel pourrait se casser. Dans votre question, vous avez mentionné des tests d'intégration; en effet, en affirmant contre le mal composée de systèmes intégrés affirmer territoire; par exemple, incompatible version de bibliothèque chargé.

Remarque, la raison pour laquelle "debug"-affirme est un compromis entre le fait d'être diligent/coffre-fort et d'être petite et rapide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X