2010-05-24 14 views
16

Ho la seguente classe che è un decoratore per un oggetto IDisposable (ho omesso la roba aggiunge) che si attua IDisposable utilizzando un modello comune:Come posso testare un finalizzatore?

public class DisposableDecorator : IDisposable 
{ 
    private readonly IDisposable _innerDisposable; 

    public DisposableDecorator(IDisposable innerDisposable) 
    { 
     _innerDisposable = innerDisposable; 
    } 

    #region IDisposable Members 

    public void Dispose() 
    { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 

    #endregion 

    ~DisposableDecorator() 
    { 
     Dispose(false); 
    } 

    protected virtual void Dispose(bool disposing) 
    { 
     if (disposing) 
      _innerDisposable.Dispose(); 
    } 
} 

I può facilmente verificare che innerDisposable è disposta quando Dispose() si chiama:

[Test] 
public void Dispose__DisposesInnerDisposable() 
{ 
    var mockInnerDisposable = new Mock<IDisposable>(); 

    new DisposableDecorator(mockInnerDisposable.Object).Dispose(); 

    mockInnerDisposable.Verify(x => x.Dispose()); 
} 

Ma come faccio a scrivere un test per assicurarsi che innerDisposable fa non ottenere disposto dal finalizzatore? Voglio scrivere qualcosa di simile, ma viene a mancare, presumibilmente perché il finalizzatore non è stato chiamato dal thread GC:

[Test] 
public void Finalizer__DoesNotDisposeInnerDisposable() 
{ 
    var mockInnerDisposable = new Mock<IDisposable>(); 

    new DisposableDecorator(mockInnerDisposable.Object); 
    GC.Collect(); 

    mockInnerDisposable.Verify(x => x.Dispose(), Times.Never()); 
} 
+0

[qui] (http://stackoverflow.com/questions/3259456/should-dispose-methods-be-unit-tested) si può vedere l'uso di IDisposable. Questo ha funzionato bene per me. –

risposta

8

Durante la scrittura dei test di unità, è necessario provare a testare il comportamento esterno, non i dettagli di implementazione. Si potrebbe obiettare che la soppressione della finalizzazione è effettivamente al di fuori del comportamento visibile, ma d'altra parte, probabilmente non c'è modo che tu possa (e non dovresti) deridere il collezionista di garabage.

Ciò che si tenta di assicurarsi nel proprio caso è che si segua una "pratica migliore" o una pratica di codifica. Dovrebbe essere applicato tramite uno strumento creato per questo scopo, ad esempio FxCop.

+1

Yup. Inoltre, non avrai mai una copertura del 100% per i tuoi test unitari. Alla fine devi solo aver fiducia che il tuo codice funzionerà, e se sei competente, dovrebbe. –

+9

In realtà avevo un bug perché avevo dimenticato di controllare il flag 'disposing' in' Dispose() ', quindi volevo aggiungere un test prima di correggerlo. – GraemeF

2

Uso Appdomain (vedere l'esempio di seguito). Classe TemporaryFile crea un file temporaneo nel costruttore e lo elimina in Dispose o in finalizer ~ TemporaryFile().

Sfortunatamente, GC.WaitForPendingFinalizers(); non mi aiuta a testare il finalizzatore.

[Test] 
    public void TestTemporaryFile_without_Dispose() 
    { 
     const string DOMAIN_NAME = "testDomain"; 
     const string FILENAME_KEY = "fileName"; 

     string testRoot = Directory.GetCurrentDirectory(); 

     AppDomainSetup info = new AppDomainSetup 
            { 
             ApplicationBase = testRoot 
     }; 
     AppDomain testDomain = AppDomain.CreateDomain(DOMAIN_NAME, null, info); 
     testDomain.DoCallBack(delegate 
     { 
      TemporaryFile temporaryFile = new TemporaryFile(); 
      Assert.IsTrue(File.Exists(temporaryFile.FileName)); 
      AppDomain.CurrentDomain.SetData(FILENAME_KEY, temporaryFile.FileName); 
     }); 
     string createdTemporaryFileName = (string)testDomain.GetData(FILENAME_KEY); 
     Assert.IsTrue(File.Exists(createdTemporaryFileName)); 
     AppDomain.Unload(testDomain); 

     Assert.IsFalse(File.Exists(createdTemporaryFileName)); 
    } 
+0

Non penso che ci sia un modo per testare correttamente un finalizzatore, dato che esiste un numero illimitato di scenari di thread in cui può essere eseguito un finalizzatore. I Finalizzatori possono finire in esecuzione su oggetti parzialmente costruiti e quindi dovrebbero essere generalmente utilizzati solo su classi abbastanza semplici da consentire la convalida di tutti gli scenari tramite ispezione.Se una classe che utilizza risorse non gestite è troppo complicata per consentire un'ispezione facile, le risorse dovrebbero essere incapsulate nella loro classe più piccola, in modo che la classe che contiene un riferimento all'oggetto che detiene le risorse non abbia bisogno di finalizzatore. – supercat

+0

Così vicino! Questo è davvero abbastanza intelligente. In effetti forza forzare il finalizzatore a correre e mi porta al 90% dove voglio essere. Tuttavia nel mio caso, devo essere in grado di utilizzare anche uno Shim Fakes e il codice in esecuzione nell'AppDomain non vede lo Shim. Non riesco a creare lo spessore all'interno del DoCallback perché sarà fuori portata prima dell'esecuzione del finalizzatore. Qualcuno l'ha capito? –

+0

@SteveInCO puoi pubblicare domande con le fonti del tuo caso? Interessante vedere la soluzione di esempio e di ricerca. – constructor

0

Non è facile per testare la messa a punto, ma può essere più facile per verificare se un oggetto è un oggetto di garbage collection.

Questo può essere fatto con riferimenti deboli.

In un test, è importante che le variabili locali siano fuori portata prima di chiamare GC.Collect(). Il modo più semplice per assicurarsi che sia un ambito di funzione.

class Stuff 
    { 
     ~Stuff() 
     { 
     } 
    } 

    WeakReference CreateWithWeakReference<T>(Func<T> factory) 
    { 
     return new WeakReference(factory()); 
    } 

    [Test] 
    public void TestEverythingOutOfScopeIsReleased() 
    { 
     var tracked = new List<WeakReference>(); 

     var referer = new List<Stuff>(); 

     tracked.Add(CreateWithWeakReference(() => { var stuff = new Stuff(); referer.Add(stuff); return stuff; })); 

     // Run some code that is expected to release the references 
     referer.Clear(); 

     GC.Collect(); 

     Assert.IsFalse(tracked.Any(o => o.IsAlive), "All objects should have been released"); 
    } 

    [Test] 
    public void TestLocalVariableIsStillInScope() 
    { 
     var tracked = new List<WeakReference>(); 

     var referer = new List<Stuff>(); 

     for (var i = 0; i < 10; i++) 
     { 
      var stuff = new Stuff(); 
      tracked.Add(CreateWithWeakReference(() => { referer.Add(stuff); return stuff; })); 
     } 

     // Run some code that is expected to release the references 
     referer.Clear(); 

     GC.Collect(); 

     // Following holds because of the stuff variable is still on stack! 
     Assert.IsTrue(tracked.Count(o => o.IsAlive) == 1, "Should still have a reference to the last one from the for loop"); 
    } 
Problemi correlati