2010-06-21 13 views
6

Recientemente, pregunté (y respondí) una pregunta sobre StackOverflow sobre por qué funcionaría una prueba unitaria cuando se ejecuta sola y luego falla esporádicamente cuando se ejecuta con todo el lote de pruebas unitarias. Vea aquí: SQL Server and TransactionScope (with MSDTC): Sporadically can't get connection¿Cómo funciona el recolector de basura con las pruebas unitarias?

Las pruebas unitarias que pasan al ejecutarse de a una por vez y luego fallan cuando se ejecutan juntas es un signo clásico de que algo está muy mal con el código.

Descubrí que hay un poco de fuga de recursos. Debido a un error sutil que causaba que las conexiones con un servidor SQL no se publicaran, me estaba quedando sin conexiones y las pruebas estaban fallando. AFAIK, esto funciona casi exactamente como una pérdida de memoria; las conexiones se asignan desde un grupo de conexiones y nunca se liberan del mismo modo que se puede asignar la memoria y luego no se libera.

Sin embargo, esto me deja con una pregunta desconcertante? ¿Cuál es la diferencia entre ejecutar mis pruebas de una en una y ejecutarlas como una suite? Si las pruebas pasan cuando se ejecutan de una en una y luego fallan cuando se ejecutan juntas, entonces debe haber algún tipo de limpieza entre las ejecuciones de prueba que ocurre solo cuando las pruebas se ejecutan de a una por vez.

Supongo que esto podría tener algo que ver con lo que el recolector de basura .net hace o no entre las pruebas. En un caso, las conexiones se liberan entre pruebas; en otro caso, no lo son.

¿Cómo puedo explicar esto?

Actualización: Para aquellos de ustedes que preguntan sobre los detalles del código, es bastante simple. Declaro un nuevo objeto TransactionScope en mi método de configuración y lo elimino en mi método Teardown. Sin embargo, la prueba del problema fue una prueba basada en datos con 100 casos de prueba; el código bajo prueba llenó un objeto SqlDataReader de una declaración de selección usando la clase SqlHelper y luego no llamó al método de cierre en el SqlDataReader. Como utilicé la clase SqlHelper para obtener el SqlDataReader, esperaba que las conexiones se manejaran por mí. ¡No tan!

Pero para aclarar, no estoy preguntando sobre mi situación específica. Lo que quiero saber es: en general, ¿cómo se liberan los recursos entre las pruebas? Me imagino que esto sería una aplicación del recolector de basura. Me pregunto si el recolector de basura todavía podría estar limpiando una prueba anterior mientras se ejecuta la siguiente prueba (¿condición de carrera?)

Actualización: Lo que sé sobre la recolección de basura con pruebas unitarias. Siguiendo mi propia curiosidad, saqué las pruebas unitarias que estaban fallando porque el objeto SqlDataReader dejó abierta una conexión. Traté de agregar System.GC.Collect() al final de cada prueba. Esto liberó con éxito las conexiones, pero impone una penalización de rendimiento del ~ 50%.

+0

Si se está quedando sin conexiones porque no las libera lo suficiente entre pruebas, sospecho que hay un problema con sus métodos de extracción. Al ejecutarlos de a uno por vez, el sistema operativo se encarga de reducir un poco, pero se ejecuta como un conjunto de aplicaciones que su uso de recursos persiste por más tiempo. – MikeD

+1

Tal vez solo necesite algunas llamadas 'Dispose' o' Close' en alguna parte. – Gabe

Respuesta

1

La recolección de basura es una tarea de fondo periódica. En específico, hay un hilo que no hace más que finalizar los objetos que ya han sido marcados como muertos. Al ejecutar una prueba a la vez, le está dando la posibilidad a ese subproceso de finalizar los objetos para cerrar las conexiones.

+0

¿De verdad?¿Puede indicarme alguna documentación que demuestre que el GC normalmente opera en su propio hilo? –

+1

http://stackoverflow.com/questions/318462/how-to-identify-the-gc-finalizer-thread –

+0

GC realmente puede detener todos los subprocesos brevemente, pero también ejecuta finalizadores en un subproceso, como se explica en el enlace. –

3

Eso suena factible, sí. No sería nada sorprendente que el marco de prueba de la unidad solicite que el recolector de basura se ejecute entre las pruebas.

Alternativamente, los diferentes patrones de ejecución pueden desencadenar naturalmente la recolección de basura cuando se ejecutan uno tras otro. El problema al analizar este tipo de cosas es que todo es muy dinámico, y variará de ejecución de prueba a ejecución de prueba.

no se olvide que probablemente no tuvo que liberar a todos los las conexiones entre las pruebas - lo suficiente para mantenerlos en funcionamiento ...

El recolector de basura en sí es poco probable que se comporten de forma diferente en las pruebas unitarias , a menos que el proceso del corredor de prueba esté configurado de una manera particular. Por otro lado, si ejecuta las pruebas en el depurador o no , afectará cuán ansioso está el recolector de basura, etc.

+0

Jon, en caso de que te lo hayas perdido, Lucero mencionó que aparentemente descarga todo el Dominio de la aplicación, lo que debería eliminar cualquier fuga. –

2

Por lo general, cada ejecución de prueba se realiza en un dominio de aplicación separado por varias razones. Ahora, cuando se descarga el appdomain, liberará los recursos asociados con él, de modo que las conexiones abiertas se cierren y, por lo tanto, impidan que se manifieste la "fuga".

Véase también Cbrumme's blog on this topic.

+0

Ah, está bien. Si descarga AppDomain, entonces no hay necesidad de forzar la recolección de basura en el medio. –

+0

Exactamente. Dado que la única forma de descargar ensamblajes es tenerlos en un dominio de aplicación separado y descargar el dominio de aplicación, la "limpieza de fuga de recursos" es un efecto secundario del mismo. – Lucero

+0

Bueno, si se trata de un efecto secundario depende de tu intención. Tuve que usar AppDomains para limpiar los recursos filtrados, y la descarga de ensamblajes es un efecto secundario. :-) –

1

Las pruebas unitarias que pasan cuando se ejecuta una a un tiempo y luego no cuando se ejecuta juntos es un signo clásico que algo está seriamente mal con el código .

Creo que hay algo muy mal con la forma en que ha escrito las pruebas de su unidad. Cada prueba debe ejecutarse independientemente de otras pruebas. Una forma de hacerlo es asegurarse de tener una configuración y métodos de desmontaje ([SetUp][TearDown]) que crean y limpian el entorno necesario para que se ejecute una prueba.

En su método de configuración, crea su conexión, en su método de desmontaje lo descarta. Ahora antes de ejecutar cada prueba, se llamará a su método de configuración y después de cada prueba se llamará a su método de desmontaje y esto asegurará que no se pierda ningún recurso.

0

¡Vaya, hay varios problemas aquí!

En primer lugar, desea que su serie de pruebas unitarias sea RÁPIDA. No golpee la base de datos para probar la lógica de negocios, etc.

En segundo lugar, si su código de producción está perdiendo recursos (?) Ese es su problema principal. No solucione ese problema cambiando la forma en que configura/desmonta su código de prueba. Ahora, si su código de prueba está asignando resorces del sistema pero no eliminando correctamente, debe corregirlo de la manera correcta y no tratar de controlar cuándo se ejecuta el recolector de elementos no utilizados. No deberías tener que preocuparte por eso.

En tercer lugar, realmente no debería tener que crear un TransactionScope en las pruebas de su unidad. Esto no tiene ningún sentido para mí. Hay algo mal con el estilo de codificación que está usando en su código de prueba. Las pruebas unitarias no son solo pruebas automatizadas, como pruebas de integración o pruebas del sistema. Las pruebas unitarias son pruebas pequeñas y enfocadas que prueban el comportamiento de una PEQUEÑA pieza de código de producción en AISLAMIENTO, que es independiente de todos los demás códigos de producción.

Ahora, un consejo sobre los recursos que se escapan. Una buena práctica de programación es usar el enunciado de uso al crear un objeto desechable para garantizar que esos recursos se desechen adecuadamente.

using (SqlDataReader reader = ...) 
{ 
    ... 
} 
+0

¿Cómo se puede usar 'SqlDataReader' con' using'. No tiene un método 'dispose()'. –

+0

Dude solo abre su buscador de objetos y lo revisa, hereda DbDataReader desechable - vea la declaración de tipo a continuación. Por cierto, la mayoría de estos tipos relacionados con la conexión a la base de datos tienen que ser desechables, ya que envuelven varios recursos del SO que se asignan cuando se usan. Cerrar un lector lo está eliminando. Una ventaja de usar sentencias es que eliminan incluso si se lanza una excepción, en efecto azúcar para try {...} finally {foo.Dispose();} public class SqlDataReader: DbDataReader, IDataReader, IDisposable, IDataRecord – Mahol25

Cuestiones relacionadas