2010-05-24 9 views
16

he la clase siguiente, que es un decorador para un objeto IDisposable (he omitido las cosas que se suma) que a su vez implementa IDisposable usando un patrón común:¿Cómo pruebo la unidad de un finalizador?

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(); 
    } 
} 

que puede probar fácilmente que innerDisposable está dispuesto cuando Dispose() se llama:

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

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

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

Pero ¿Cómo se escribe una prueba para asegurarse de que no innerDisposable qué conseguir dispuesto por el finalizador? Quiero escribir algo como esto pero fracasa, presumiblemente porque el finalizador no ha sido llamado por el hilo de 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

[aquí] (http://stackoverflow.com/questions/3259456/should-dispose-methods-be-unit-tested) se puede ver el uso de IDisposable. Eso funcionó bien para mí. –

Respuesta

8

Al escribir las pruebas unitarias, siempre debe intentar probar el comportamiento externo visible, no los detalles de implementación. Se podría argumentar que suprimir la finalización está fuera del comportamiento visible, pero, por otro lado, probablemente no hay forma de que puedas (ni deberías) burlarte del coleccionista de garabatos.

Lo que intenta asegurarse en su caso, es que se sigan las "mejores prácticas" o una práctica de codificación. Debe hacerse cumplir a través de una herramienta que está hecha para este fin, como FxCop.

+1

Sí. Además, nunca obtendrás una cobertura del 100% para tus pruebas unitarias. Eventualmente solo debes tener fe en que tu código funcionará, y si eres competente, debería hacerlo. –

+9

En realidad, tenía un error porque había olvidado comprobar el indicador 'desechar' en 'Dispose()', así que quería agregar una prueba antes de solucionarlo. – GraemeF

2

Utilizo Appdomain (vea la muestra a continuación). Clase TemporaryFile crea un archivo temporal en el constructor y lo elimina en Dispose o en el finalizador ~ TemporaryFile().

Desafortunadamente, GC.WaitForPendingFinalizers(); no me ayuda a probar el finalizador.

[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

No creo que haya ninguna manera de probar correctamente un finalizador, dado que hay un número ilimitado de escenarios de subprocesamiento bajo los cuales se puede ejecutar un finalizador. Los finalizadores pueden terminar ejecutados en objetos parcialmente construidos y, por lo tanto, generalmente solo deben usarse en clases que sean lo suficientemente simples como para permitir que todos los escenarios se validen a través de la inspección.Si una clase que utiliza recursos no administrados es demasiado complicada para permitir una inspección fácil, los recursos deben estar encapsulados en su propia clase más pequeña, de modo que la clase que contiene una referencia al objeto que contiene los recursos no necesitará el finalizador. – supercat

+0

¡Tan cerca! Esto es realmente bastante inteligente. De hecho obliga al finalizador a ejecutarse, y me lleva al 90% a donde quería estar. Sin embargo, en mi caso, también debo poder usar Fakes Shim, y el código que se ejecuta en AppDomain no ve el Shim. No puedo crear el ajuste dentro de DoCallback porque estará fuera del alcance antes de que se ejecute el finalizador. ¿Alguien ha pensado en eso? –

+0

@SteveInCO ¿podría publicar una pregunta con las fuentes de su caso? Interesante para ver la muestra y la solución de búsqueda. – constructor

0

No es fácil poner a prueba la finalización, pero puede ser más fácil de probar si un objeto es un objeto de la recolección de basura.

Esto se puede hacer con una referencia débil.

En una prueba, es importante que las variables locales se queden fuera del alcance antes de llamar a GC.Collect(). La forma más fácil de asegurarse es un alcance de función.

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"); 
    } 
Cuestiones relacionadas