Beste werkwijze voor het debuggen van Asserts tijdens het testen van eenheden

Ontmoedigt intensief gebruik van unit-tests het gebruik van debug-claims? Het lijkt erop dat een debug-bewering die in de te testen code wordt geactiveerd, impliceert dat de unit-test niet zou moeten bestaan ​​of dat de debug-bewering niet zou moeten bestaan. “Er kan er maar één zijn” lijkt een redelijk principe. Is dit de gangbare praktijk? Of schakelt u uw debug-claims uit bij het testen van eenheden, zodat ze in de buurt kunnen zijn voor integratietests?

Bewerken: ik heb ‘Assert’ geüpdatet om assertief te debuggen om een ​​assert in de te testen code te onderscheiden van de regels in de unit-test die de status controleren nadat de test is uitgevoerd.

Hier is ook een voorbeeld dat volgens mij het dilemma laat zien:
Een eenheidstest geeft ongeldige invoer door voor een beveiligde functie die beweert dat de invoer geldig is. Zou de eenheidstest niet moeten bestaan? Het is geen publieke functie. Misschien zou het controleren van de ingangen perf doden? Of zou de bewering niet moeten bestaan? De functie is beveiligd en niet privé, dus het zou de ingangen moeten controleren op veiligheid.


Antwoord 1, autoriteit 100%

Dit is een volkomen terechte vraag.

Allereerst suggereren veel mensen dat je beweringen verkeerd gebruikt. Ik denk dat veel debugging-experts het daar niet mee eens zijn. Hoewel het een goede gewoonte is om invarianten te controleren met beweringen, moeten beweringen niet worden beperkt tot toestandsinvarianten. Veel deskundige debuggers zullen u zelfs vertellen om eventuele voorwaarden te stellen die een uitzondering kunnen veroorzaken, naast het controleren van invarianten.

Beschouw bijvoorbeeld de volgende code:

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

Dat is prima. Maar wanneer de uitzondering wordt gegenereerd, wordt de stapel afgewikkeld totdat iets de uitzondering afhandelt (waarschijnlijk een standaard handler op het hoogste niveau). Als de uitvoering op dat moment pauzeert (u hebt mogelijk een dialoogvenster voor modaal uitzonderingen in een Windows-app), hebt u de kans om een ​​debugger toe te voegen, maar u bent waarschijnlijk veel van de informatie kwijt die u had kunnen helpen om het probleem op te lossen, omdat het grootste deel van de stapel is afgewikkeld.

Overweeg nu het volgende:

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

Als het probleem zich nu voordoet, verschijnt het modale assert-dialoogvenster. De uitvoering wordt onmiddellijk onderbroken. U bent vrij om de door u gekozen debugger toe te voegen en precies te onderzoeken wat er op de stapel staat en de toestand van het systeem op het exacte punt van storing. In een release-build krijg je nog steeds een uitzondering.

Hoe gaan we nu om met uw unit-tests?

Overweeg een eenheidstest die de bovenstaande code test die de bewering bevat. U wilt controleren of de uitzondering wordt gegenereerd wanneer param1 null is. Je verwacht dat die bepaalde bewering zal mislukken, maar alle andere mislukte beweringen zouden erop wijzen dat er iets mis is. U wilt bepaalde mislukte beweringen toestaan ​​voor bepaalde tests.

De manier waarop u dit oplost, hangt af van de talen enz. die u gebruikt. Ik heb echter enkele suggesties als je .NET gebruikt (ik heb dit niet echt geprobeerd, maar ik zal in de toekomst het bericht bijwerken):

  1. Controleer Trace.Listeners. Zoek een instantie van DefaultTraceListener en stel AssertUiEnabled in op false. Dit zorgt ervoor dat het modale dialoogvenster niet meer verschijnt. Je zou ook de verzameling luisteraars kunnen wissen, maar je krijgt geen enkele tracering.
  2. Schrijf uw eigen TraceListener waarin beweringen worden vastgelegd. Hoe u beweringen vastlegt, is aan u. Het opnemen van het foutbericht is misschien niet goed genoeg, dus misschien wilt u de stapel doorlopen om de methode te vinden waar de bewering vandaan kwam en dat ook op te nemen.
  3. Zodra een test is afgelopen, controleert u of de enige mislukte beweringen die zijn opgetreden, de fouten waren die u verwachtte. Als er nog andere zijn opgetreden, faal dan voor de test.

Voor een voorbeeld van een TraceListener die de code bevat om zo’n stackwalk te doen, zou ik naar SuperAssertListener van SUPERASSERT.NET zoeken en de code ervan controleren. (Het is ook de moeite waard om SUPERASSERT.NET te integreren als je echt serieus bezig bent met het debuggen met behulp van beweringen).

De meeste unit-testframeworks ondersteunen methoden voor het opzetten/afbreken van tests. Misschien wilt u code toevoegen om de traceerlistener opnieuw in te stellen en te bevestigen dat er geen onverwachte mislukte beweringen zijn in die gebieden om duplicatie te minimaliseren en fouten te voorkomen.

UPDATE:

Hier is een voorbeeld van TraceListener dat kan worden gebruikt om beweringen te testen. U moet een instantie toevoegen aan de Trace.Listeners-verzameling. U wilt waarschijnlijk ook een gemakkelijke manier bieden waarop uw tests de luisteraar kunnen bereiken.

OPMERKING: dit heeft veel te danken aan 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();
    }
}

U kunt aan het begin van elke test beweringen toevoegen aan de collectie AllowedFailures voor de beweringen die u verwacht.

Doe aan het einde van elke test (hopelijk ondersteunt uw unit-testraamwerk een test-demontagemethode):

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

Antwoord 2, autoriteit 44%

IMHO debug.beweert rock. Dit geweldige artikel laat zien hoe u kunt voorkomen dat ze uw unit-test onderbreken door een app.config toe te voegen aan uw unit-testproject en het dialoogvenster uit te schakelen:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
    <assert assertuienabled="false"/>
</system.diagnostics>

Antwoord 3, autoriteit 24%

Zoals anderen al hebben vermeld, zijn beweringen van Debug bedoeld voor dingen die altijd waar moeten zijn. (De mooie term hiervoor is invarianten).

Als uw eenheidstest valse gegevens doorgeeft die de bewering over de kop slaan, moet u uzelf de vraag stellen: waarom gebeurt dat?

  • Als de te testen functie verondersteltom te gaan met de nepgegevens, dan zou die bewering er duidelijk niet moeten zijn.
  • Als de functie nietis uitgerust om met dat soort gegevens om te gaan (zoals aangegeven door de bewering), waarom test je er dan een eenheid voor?

Het tweede punt is er een waar nogal wat ontwikkelaars in lijken te vervallen. Unit-test de beste uit alle dingen waar uw code op is gebouwd, en beweer of gooi uitzonderingen voor al het andere – Immers, als uw code NIET is gebouwd om met die situaties om te gaan, en u ervoor zorgt dat ze gebeuren, wat doet u dan? verwacht je dat het gaat gebeuren?
Kent u die delen van de C/C++-documentatie die spreken over “ongedefinieerd gedrag”? Dit is het. Borg en borg hard.


Update om te verduidelijken: de keerzijde hiervan is dat je je uiteindelijk realiseert dat je Debug.Assertalleen moet gebruiken voor interne dingen die andere interne dingen noemen.
Als uw code wordt blootgesteld aan derden (dwz het is een bibliotheek of zoiets), dan is er geen limiet aan de invoer die u kunt verwachten, en daarom moet u correct valideren en uitzonderingen maken of wat dan ook, en u moet daar ook voor testen


Antwoord 4, autoriteit 20%

Beweringen in uw code zijn (zou moeten zijn) uitspraken aan de lezer die zeggen “deze voorwaarde zou op dit punt altijd waar moeten zijn.” Als ze met enige discipline worden gedaan, kunnen ze ervoor zorgen dat de code correct is; de meeste mensen gebruiken ze als debug-afdrukinstructies. Unit Tests zijn code die aantoontdat uw code een bepaalde testcase correct uitvoert; Nou, ze kunnen allebei de vereisten documenteren en je vertrouwen wekken dat de code inderdaad correct is.

Ken je het verschil? De beweringen van het programma helpen je om het correct te maken, de unit-tests helpen je het vertrouwen van iemand anders te ontwikkelen dat de code correct is.


Antwoord 5, autoriteit 5%

Een goede testopstelling voor eenheden kan beweringen opvangen. Als een assert wordt geactiveerd, moet de huidige test mislukken en wordt de volgende uitgevoerd.

In onze bibliotheken hebben debug-functionaliteit op laag niveau, zoals TTY/ASSERTS, handlers die worden aangeroepen. De standaard handler zal printf/break, maar klantcode kan aangepaste handlers installeren voor ander gedrag.

Ons UnitTest-framework installeert zijn eigen handlers die berichten loggen en uitzonderingen op beweringen genereren. De UnitTest-code vangt deze uitzonderingen vervolgens op als ze zich voordoen en registreert ze als mislukt, samen met de beweerde instructie.

U kunt ook assert-testen opnemen in uw eenheidstest – bijvoorbeeld

CHECK_ASSERT(someList.getAt(someList.size() + 1); // test slaagt als een bewering plaatsvindt


Antwoord 6, autoriteit 5%

Ik koos ervoor om de bewering alleen uit te schakelen waar nodig, in plaats van het projectbreed te doen. Hier is een benadering waarbij de bewering kan worden opgeschort, zodat deze de teststroom niet verstoort.

public static class TraceListenerCollectionEx
{
    /// <summary>
    /// This is a helper class that allows us to suspend asserts / all trace listeners
    /// </summary>
    public class SuspendTrackerDisposable : IDisposable
    {
        private readonly TraceListenerCollection _traceListenerCollection;
        private readonly TraceListener[] _suspendedListeners;
        public SuspendTrackerDisposable(TraceListenerCollection traceListenerCollection)
        {
            _traceListenerCollection = traceListenerCollection;
            var numListeners = traceListenerCollection.Count;
            _suspendedListeners = new TraceListener[numListeners];
            for( int index = 0; index < numListeners; index += 1 )
                _suspendedListeners[index] = traceListenerCollection[index];
            traceListenerCollection.Clear();
        }
        public void Dispose()
        {
            _traceListenerCollection.AddRange(_suspendedListeners);
        }
    }
    public static SuspendTrackerDisposable AssertSuspend(this TraceListenerCollection traceListenerCollection) => new SuspendTrackerDisposable(traceListenerCollection);
}

Hier is een voorbeeld van gebruik binnen een test:

   [TestMethod]
    public void EnumDefaultTest()
    {
        using(Trace.Listeners.AssertSuspend()) {
            Enum<CarClass>.DefaultValue.ShouldBe(CarClass.Unknown);  
        }
    }

Voor de code die wordt uitgevoerd binnen het gebruiksblok, in dit geval slechts één regel, zijn de beweringen uitgeschakeld.


Antwoord 7, autoriteit 2%

Bedoel je C++/Java beweringen voor “programmeren op contract” beweringen, of CppUnit/JUnit beweert? Die laatste vraag doet me geloven dat het de eerste is.

Interessante vraag, omdat ik begrijp dat deze beweringen vaak worden uitgeschakeld tijdens runtime wanneer je ze in productie zet. (Een beetje verslaat het doel, maar dat is een andere vraag.)

Ik zou zeggen dat ze in uw code moeten worden achtergelaten wanneer u deze test. Je schrijft toetsen om ervoor te zorgen dat de randvoorwaarden goed worden gehandhaafd. De test moet een “zwarte doos” zijn; je zou als een cliënt voor de klas moeten optreden wanneer je test. Als je ze in productie uitschakelt, maakt dat de tests niet ongeldig.


Antwoord 8, autoriteit 2%

Als je eerst zowel Design by Contract-beweringen enunittests hebt, zal je unit-testraamwerk de beweringen kunnen opvangen. Als uw unit-tests worden afgebroken vanwege een DbC-abort, kunt u ze eenvoudigweg niet uitvoeren. Het alternatief hier is om die beweringen uit te schakelen tijdens het uitvoeren (lees compileren) van uw eenheidstests.

Aangezien u niet-openbare functies test, wat is dan het risico dat een functie wordt aangeroepen met een ongeldig argument? Dekken uw unit tests dat risico niet? Als u uw code schrijft volgens de TDD-techniek (Test-Driven Development), zouden ze dat moeten doen.

Als je die beweringen van het Dbc-type echt in je code wilt/nodig hebt, dan kun je de eenheidstests verwijderen die de ongeldige argumenten doorgeven aan de methoden met die beweringen.

Beweringen van het Dbc-type kunnen echter nuttig zijn in functies op een lager niveau (die niet direct worden aangeroepen door de unit-tests) wanneer je grofkorrelige unit-tests hebt.


Antwoord 9, autoriteit 2%

U moet uw beweringen over debuggen behouden, zelfs met unit-tests.

Het probleem hier is geen onderscheid maken tussen fouten en problemen.

Als een functie zijn argumenten controleert die onjuist zijn, zou dit niet moeten resulteren in een foutopsporingsbevestiging. In plaats daarvan zou het een foutwaarde terug moeten geven. Het was een fout om de functie met verkeerde parameters aan te roepen.

Als een functie correcte gegevens krijgt doorgegeven, maar niet correct kan werken omdat de runtime onvoldoende geheugen heeft, dan zou de code een foutopsporingsbevestiging moeten afgeven vanwege dit probleem. Dat is een voorbeeld van fundamentele veronderstellingen die, als ze niet kloppen, “alle weddenschappen zijn uitgeschakeld”, dus u moet beëindigen.

Schrijf in jouw geval de eenheidstest die foutieve waarden als argumenten levert. Het zou een foutretourwaarde (of iets dergelijks) moeten verwachten. Een bewering krijgen? — refactor de code om in plaats daarvan een fout te produceren.

Let op: een probleem zonder bugs kan nog steeds beweringen veroorzaken; bijv. de hardware kan breken. In uw vraag noemde u integratietesten; inderdaad, beweren tegen onjuist samengestelde geïntegreerde systemen is territorium; bijv. incompatibele bibliotheekversie geladen.

Let op, de reden voor “debug”-beweringen is een afweging tussen ijverig/veilig en snel/klein zijn.


Antwoord 10

Zoals de anderen al hebben gezegd, moeten de Debug.Assert-statements altijd truezijn, zelfs als argumenten onjuist zijn, moet de bewering waar zijn om te voorkomen dat de app in een ongeldige staat enz.

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

Je zou dit niet moeten kunnen testen (en waarschijnlijk ook niet willen omdat je eigenlijk niets kunt doen!)

Ik zou er helemaal naast kunnen zitten, maar ik heb de indruk dat de beweringen waar je het over hebt misschien beter geschikt zijn als “argumentuitzonderingen”, bijvoorbeeld

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

Dat soort “bewering” in uw code is nog steeds zeer testbaar.

PK 🙂


Antwoord 11

Het is een tijdje geleden dat deze vraag is gesteld, maar ik denk dat ik een andere manier heb om Debug.Assert()-aanroepen te verifiëren vanuit een unit-test met behulp van C#-code. Let op het #if DEBUG ... #endif-blok, dat nodig is om de test over te slaan als het niet in de foutopsporingsconfiguratie wordt uitgevoerd (in dat geval wordt Debug.Assert() sowieso niet geactiveerd).

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

De klasse UnitTestTraceListener ziet er als volgt uit:

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

Other episodes