Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Código asincrónico de pruebas de unidad: tres soluciones para mejorar las pruebas
Descargar el código de muestra
La programación asincrónica se ha vuelto cada vez más importante durante la última década. Ya sea por el paralelismo basado en la CPU o la simultaneidad basada en E/S, los desarrolladores están empleando la asincronía para ayudar a hacer disponibles la mayoría de los recursos y, en última instancia, lograr más con menos. Las aplicaciones cliente que responden mejor y las aplicaciones de servidor más escalables están todas al alcance de la mano.
Los desarrolladores de software han aprendido muchos patrones de diseño para crear de manera efectiva funcionalidad sincrónica, pero las prácticas recomendadas para diseñar software asincrónico son relativamente nuevas, aunque el soporte proporcionado por las bibliotecas y los lenguajes de programación para programación paralela y simultánea han mejorado drásticamente con el lanzamiento de Microsoft .NET Framework 4 y 4.5. Si bien ya hay bastantes buenos consejos para usar las nuevas técnicas (vea “Prácticas recomendadas en la programación asincrónica” en bit.ly/1ulDCiI y “Charla: prácticas recomendadas de asincronía” en bit.ly/1DsFuMi), las prácticas recomendadas para diseñar las API internas y externas para aplicaciones y bibliotecas con características de idiomas como async y await y la TPL (biblioteca de procesamiento paralelo basado en tareas) son todavía desconocidas para muchos desarrolladores.
Este vacío afecta no solo al rendimiento y a la confiabilidad de las aplicaciones y bibliotecas que los desarrolladores están creando, sino también la capacidad de someter a pruebas sus soluciones, ya que muchas de las prácticas recomendadas que habilitan la creación de los sólidos diseños asincrónicos habilitan también las pruebas de unidad más sencillas.
Teniendo en cuenta dichas prácticas recomendadas, en este artículo se presentarán maneras de diseñar y refactorizar código para una mejor capacidad de someter a pruebas y demostrará la manera en que esto influirá en las pruebas. Las soluciones son aplicables al código que se beneficia de async y await, así como código basado en mecanismos de multithreading de nivel inferior de marcos y bibliotecas anteriores. Y, en el proceso, las soluciones no solo se factorizarán para pruebas, los usuarios del código desarrollado las consumirán con mayor facilidad y eficiencia.
El equipo con el que trabajo está desarrollando software para dispositivos médicos de rayos X. En este dominio es fundamental que nuestra cobertura de prueba de unidad se encuentre siempre en un nivel alto. Hace poco tiempo un desarrollador me preguntó: “Siempre nos presionas para escribir pruebas de unidad para todo nuestro código. Sin embargo, ¿cómo puedo escribir pruebas de unidad razonables cuando mi código empieza otro subproceso o está usando un temporizador que más tarde empieza un subproceso y lo ejecuta varias veces?”.
Esa pregunta tiene sentido. Supongamos que tengo este código para probarlo:
public void StartAsynchronousOperation()
{
Message = "Init";
Task.Run(() =>
{
Thread.Sleep(1900);
Message += " Work";
});
}
public string Message { get; private set; }
Mi primer intento de escribir una prueba no fue muy prometedor:
[Test]
public void FragileAndSlowTest()
{
var sut = new SystemUnderTest();
// Operation to be tested will run in another thread.
sut. StartAsynchronousOperation();
// Bad idea: Hoping that the other thread finishes execution after 2 seconds.
Thread.Sleep(2000);
// Assert outcome of the thread.
Assert.AreEqual("Init Work", sut.Message);
}
Espero que las pruebas de unidad se ejecuten rápido y ofrezcan resultados predecibles, pero la prueba que escribí era frágil y lenta. El método StartAsynchronousOperation arranca la operación que se va a probar en otro subproceso y la prueba debería comprobar el resultado de la operación. El tiempo que se tarda en iniciar un nuevo subproceso o en inicializar un subproceso existente que se encuentra en el grupo de subprocesos y en ejecutar la operación no es predecible, ya que depende de otros procesos que se ejecutan en la máquina de pruebas. Se puede producir un error en la prueba de vez en cuando cuando la suspensión es demasiado breve y la operación asincrónica no ha terminado todavía. Estoy entre la espada y la pared: O bien intento mantener el tiempo de espera lo más breve posible, con el riesgo de una prueba fácil, o bien aumento el tiempo de suspensión para que la prueba sea más sólida, pero ralentizo aún más la prueba.
El problema es similar cuando quiero probar código que usa un temporizador:
private System.Threading.Timer timer;
private readonly Object lockObject = new Object();
public void StartRecurring()
{
Message = "Init";
// Set up timer with 1 sec delay and 1 sec interval.
timer = new Timer(o => { lock(lockObject){ Message += " Poll";} }, null,
new TimeSpan(0, 0, 0, 1), new TimeSpan(0, 0, 0, 1));
}
public string Message { get; private set; }
Y es probable que esta prueba tenga los mismos problemas:
[Test]
public void FragileAndSlowTestWithTimer()
{
var sut = new SystemUnderTest();
// Execute code to set up timer with 1 sec delay and interval.
sut.StartRecurring();
// Bad idea: Wait for timer to trigger three times.
Thread.Sleep(3100);
// Assert outcome.
Assert.AreEqual("Init Poll Poll Poll", sut.Message);
}
Cuando pruebo una operación razonablemente compleja con varias ramas de código diferentes, termino con un gran número de pruebas independientes. Mis conjuntos de pruebas se ralentizan cada vez más con cada prueba nueva. Los costos de mantenimiento de estos conjuntos de pruebas aumentan pero tengo que pasar tiempo investigando los errores esporádicos. Además, los conjuntos de pruebas lentos tienden a ejecutarse solo poco a menudo y por tanto tienen menores ventajas. En algún punto, probablemente dejaría de ejecutar del todo estas pruebas lentas y de errores intermitentes.
Los dos tipos de pruebas mostrados anteriormente también pueden detectar cuándo la operación genera una excepción. Debido a que la operación se ejecuta en un subproceso diferente, las excepciones no se propagan al subproceso del ejecutor de pruebas. Esto limita la capacidad de las pruebas para comprobar el comportamiento correcto de los errores del código que se está probando.
Voy a presentar tres soluciones generales para evitar las pruebas de unidad lentas y frágiles mejorando el diseño del código probado y mostraré cómo esto habilita las pruebas de unidad para comprobar excepciones. Cada solución tiene ventajas, además de desventajas o limitaciones. Al final, daré algunas recomendaciones respecto a qué solución elegir para diferentes situaciones.
Este artículo trata de las pruebas de unidad. Normalmente, las pruebas de unidad son código de pruebas de forma aislada de otras partes del sistema. Una de las demás partes del sistema es la capacidad multithreading de SO. Los métodos y las clases de biblioteca estándar se usan para programar trabajo asincrónico pero se debe excluir el aspecto multithreading en las pruebas de unidad, que deben concentrarse en la funcionalidad que se ejecuta de manera asincrónica.
Las pruebas de unidad para código asincrónico tienen sentido cuando el código contiene fragmentos de funcionalidad que se ejecutan en un subproceso y las pruebas de unidad deben comprobar que los fragmentos funcionan de la manera esperada. Cuando las pruebas de unidad han mostrado que la funcionalidad es correcta, tiene sentido usar estrategias de pruebas adicionales para descubrir problemas de simultaneidad. Hay varios enfoques para pruebas y análisis de código multiproceso con el fin de encontrar estos tipos de problemas (vea, por ejemplo, “Herramientas y técnicas para identificar problemas de simultaneidad”, en bit.ly/1tVjpll). Por ejemplo, las pruebas de esfuerzo, pueden colocar un sistema completo o una gran parte del sistema bajo carga. Estas estrategias son razonables para complementar pruebas de unidad pero están fuera del ámbito de este artículo. En las soluciones de este artículo se mostrará cómo excluir las partes de multithreading a la vez que se prueba de forma aislada la funcionalidad con pruebas de unidad.
Solución 1: Funcionalidad independiente de multithreading
La solución más sencilla para las pruebas de unidad de la funcionalidad implicada en operaciones asincrónicas es separar dicha funcionalidad de multithreading. Gerard Mezaros ha descrito este enfoque en el patrón de objeto humilde en su libro, “xUnit Test Patterns” (Addison-Wesley, 2007). La funcionalidad que se va a probar se extrae en una nueva clase independiente y la parte multithreading permanece dentro de objeto humilde, que llama a la nueva clase (vea la Figura 1).
Figura 1. El patrón de objeto humilde
En el siguiente código se muestra la funcionalidad extraída después de la refactorización, que es puramente código sincrónico:
public class Functionality
{
public void Init()
{
Message = "Init";
}
public void Do()
{
Message += " Work";
}
public string Message { get; private set; }
}
Antes de la refactorización, la funcionalidad se mezcló con código asincrónico, pero lo he movido a la clase Funcionalidad. Esta clase se puede probar ahora mediante pruebas de unidad sencillas porque no contiene ya ningún multithreading. Tenga en cuenta que dicho refactoring es importante en general, no solo para pruebas de unidad: los componentes no deben exponer contenedores asincrónicos para operaciones inherentemente sincrónicas y deben en su lugar dejar en manos del autor de la llamada la decisión de si desea descargar la invocación de dicha operación. En el caso de una prueba de unidad, elijo no hacerlo, pero la aplicación de consumo puede decidir hacerlo, por motivos de capacidad de respuesta o ejecución paralela. Para obtener más información, vea la publicación del blog de Stephen Toub, “¿Debo exponer a los contenedores asincrónico para métodos sincrónicos?” (bit.ly/1shQPfn).
En una variación ligera de este patrón, se pueden hacer públicos determinados métodos privados de la clase SystemUnderTest para permitir pruebas para llamar a estos métodos directamente. En este caso, no se tiene que crear ninguna clase adicional para probar la funcionalidad sin multithreading.
La separación de la funcionalidad mediante el patrón de objeto humilde es sencilla y se puede realizar no solo para código que programa de inmediato trabajo asincrónico una vez sino también para código que usa temporizadores. En ese caso, el control del temporizador se conserva en el objeto humilde y la operación recurrente se mueve a la clase Functionality o a un método público. Una ventaja de esta solución es que las pruebas pueden comprobar directamente excepciones por el código que se están probando. El patrón de objeto humilde se puede aplicar con independencia de las técnicas usadas para programar el trabajo asincrónico. Los inconvenientes de esta solución son que no se prueba el propio objeto humilde y que se tiene que modificar el código que se está probando.
Solución 2: Sincronizar pruebas
Si la prueba puede detectar la finalización de la operación que se está probando, que se ejecuta de manera asincrónica, puede evitar los dos inconvenientes: la fragilidad y la lentitud. Aunque la prueba ejecuta código multiproceso, puede ser confiable y rápida cuando la prueba se sincroniza con la operación programada por el código que se está probando. La prueba puede concentrarse en la funcionalidad mientras se minimizan los efectos negativos de la ejecución asincrónica.
En el mejor caso, el método que se está probando devuelve una instancia de un tipo que se señalizará cuando haya finalizado la operación. El tipo Task, que ha estado disponible en .NET Framework desde la versión 4, cumple bien esta necesidad y la característica async/await disponible desde .NET Framework 4.5 simplifica la creación de Tareas:
public async Task DoWorkAsync()
{
Message = "Init";
await Task.Run( async() =>
{
await Task.Delay(1900);
Message += " Work";
});
}
public string Message { get; private set; }
Esta refactorización representa una práctica recomendada general que ayuda tanto al caso de pruebas de unidad como al consumo general de la funcionalidad asincrónica expuesta. Al devolver una Task que representa la operación asincrónica, un consumidor del código puede determinar con facilidad cuándo ha finalizado la operación asincrónica, si tuvo un error con una excepción y si ha devuelto un error.
Esto hace de las pruebas de unidad un método asincrónico tan simple como de las pruebas de unidad un método sincrónico. Ahora es sencillo que la prueba se sincronice con el código que se está probando con solo invocar el método objetivo y esperar a que finalice la Task devuelta. Esta espera se puede realizar de manera sincrónica (bloqueando el subproceso de llamada) mediante los método Task Wait o se puede realizar de manera asincrónica (mediante continuaciones para evitar el bloque del subproceso de llamada) con la palabra clave await, antes de comprobar el resultado de la operación asincrónica (vea la Figura 2).
Figura 2. Sincronización mediante Async y Await
Para usar await en un método de pruebas de unidad, la propia prueba se tiene que declarar con async en su firma. Ya no se necesita ninguna instrucción de suspensión:
[Test]
public async Task SynchronizeTestWithCodeViaAwait()
{
var sut = new SystemUnderTest();
// Schedule operation to run asynchronously and wait until it is finished.
await sut.StartAsync();
// Assert outcome of the operation.
Assert.AreEqual("Init Work", sut.Message);
}
Afortunadamente, las versiones más recientes de los marcos de pruebas de unidad principales —MSTest, xUnit.net y NUnit— admiten las pruebas async y await (vea el blog de Stephen Cleary en bit.ly/1x18mta). Sus ejecutores de pruebas pueden hacer frente a las pruebas de tareas asincrónicas y esperar la finalización del subproceso antes de empezar a evaluar las instrucciones Assert. Si el ejecutor del marco de pruebas de unidad no puede hacer frente a las firmas del método de pruebas de la tarea asincrónica, la prueba puede llamar al menos al método Wait de la Task devuelta del sistema que se está probando.
Además, se puede mejorar la funcionalidad basada en el temporizador con la ayuda de la clase TaskCompletionSource (vea los detalles en la descarga del código). La prueba puede esperar entonces la finalización de operaciones periódicas específicas:
[Test]
public async Task SynchronizeTestWithRecurringOperationViaAwait()
{
var sut = new SystemUnderTest();
// Execute code to set up timer with 1 sec delay and interval.
var firstNotification = sut.StartRecurring();
// Wait that operation has finished two times.
var secondNotification = await firstNotification.GetNext();
await secondNotification.GetNext();
// Assert outcome.
Assert.AreEqual("Init Poll Poll", sut.Message);
}
Lamentablemente, a veces el código que se está probando no puede usar async y await, como cuando está probando código que ya se ha enviado y por motivos de cambio brusco no se puede cambiar la firma del método que se está probando. En situaciones como esta, la sincronización se tiene que implementar con otras técnicas. La sincronización se puede realizar si la clase que se está probando invoca un evento o llama a un objeto dependiente cuando finaliza la operación. En el siguiente ejemplo se muestra cómo implementar la prueba cuando se llama a un objeto dependiente:
private readonly ISomeInterface dependent;
public void StartAsynchronousOperation()
{
Task.Run(()=>
{
Message += " Work";
// The asynchronous operation is finished.
dependent.DoMore()
});
}
Un ejemplo adicional de la sincronización basada en eventos se encuentra en la descarga del código.
La prueba se puede sincronizar ahora con la operación asincrónica cuando se reemplaza el objeto dependiente por código auxiliar durante las pruebas (vea la Figura 3).
Figura 3. Sincronización mediante un código auxiliar de objeto dependiente
La prueba tiene que equipar el código auxiliar con un mecanismo de notificación para subprocesos porque el código auxiliar se ejecuta en otro subproceso. En el siguiente ejemplo del código de pruebas, se usa un ManualResetEventSlim y se genera el código auxiliar con el marco de simulación RhinoMocks:
// Setup
var finishedEvent = new ManualResetEventSlim();
var dependentStub = MockRepository.GenerateStub<ISomeInterface>();
dependentStub.Stub(x => x.DoMore()).
WhenCalled(x => finishedEvent.Set());
var sut = new SystemUnderTest(dependentStub);
La prueba puede ejecutar ahora la operación asincrónica y esperar la notificación:
// Schedule operation to run asynchronously.
sut.StartAsynchronousOperation();
// Wait for operation to be finished.
finishedEvent.Wait();
// Assert outcome of operation.
Assert.AreEqual("Init Work", sut.Message);
Esta solución para sincronizar la prueba con los subprocesos probados se puede aplicar al código con determinadas características: El código que se está probando tiene un mecanismo de notificación como async y await o un evento normal, o el código llama a un objeto dependiente.
Una gran ventaja de la sincronización async y await es que puede propagar cualquier tipo de resultado de nuevo al cliente de llamada. Un tipo especial de resultado es una excepción. Por tanto, la prueba puede controlar excepciones de manera explícita. Los demás mecanismos de sincronización solo pueden reconocer errores indirectamente mediante los resultados defectuosos.
El código con funcionalidad basada en temporizador puede utilizar async/await, eventos o llamadas a objetos dependientes para permitir que las pruebas se sincronicen con las operaciones del temporizador. Cada vez que termina la operación periódica, se notifica la prueba que puede comprobar el resultado (vea ejemplos en la descarga del código).
Lamentablemente, los temporizadores ralentizan las pruebas incluso cuando usa una notificación. La operación periódica que quiere probar generalmente empieza solo con un retraso determinado. La prueba se ralentizará y tardará al menos el tiempo del retraso. Este es un inconveniente adicional además del requisito previo de notificación.
Ahora veremos una solución que evita algunas de las limitaciones de las dos anteriores.
Solución 3: Probar un subproceso
Para esta solución, se tiene que preparar el código que se está probando de una manera que la prueba pueda desencadenar más tarde directamente la ejecución de las operaciones del mismo subproceso de la propia prueba. Esta es una traducción del enfoque del equipo de jMock para Java (vea “Prueba del código multiproceso” en jmock.org/threads.html).
El sistema que se está probando en el siguiente ejemplo usa un objeto programador de tareas inyectado para programar el trabajo asincrónico. Para demostrar las capacidades de la tercera solución, he agregado una segunda solución que se iniciará cuando finalice la primera:
private readonly TaskScheduler taskScheduler;
public void StartAsynchronousOperation()
{
Message = "Init";
Task task1 = Task.Factory.StartNew(()=>{Message += " Work1";},
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler);
task1.ContinueWith(((t)=>{Message += " Work2";}, taskScheduler);
}
El sistema que se está probando se modifica para usar un TaskScheduler independiente. Durante la prueba, el TaskScheduler “normal” se reemplaza por un DeterministicTaskScheduler, que permite el inicio de las operaciones asincrónicas de manera sincrónica (vea la Figura 4).
Figura 4. Usar un TaskScheduler independiente en SystemUnderTest
La prueba siguiente puede ejecutar las operaciones programadas en el mismo subproceso que la propia prueba. La prueba inyecta el DeterministicTaskScheduler en el código que se está probando. El DeterministicTaskScheduler no genera un nuevo proceso de inmediato sino que solo pone en cola las tareas programadas. En la siguiente instrucción el método RunTasksUntilIdle ejecuta las dos operaciones de manera sincrónica:
[Test]
public void TestCodeSynchronously()
{
var dts = new DeterministicTaskScheduler();
var sut = new SystemUnderTest(dts);
// Execute code to schedule first operation and return immediately.
sut.StartAsynchronousOperation();
// Execute all operations on the current thread.
dts.RunTasksUntilIdle();
// Assert outcome of the two operations.
Assert.AreEqual("Init Work1 Work2", sut.Message);
}
El DeterministicTaskScheduler invalida los métodos TaskScheduler para proporcionar la funcionalidad de programación y agrega entre otros el método RunTasksUntilIdle de manera específica para pruebas (vea la descarga del código para detalles de implementación de DeterministicTaskScheduler). Como en las pruebas de unidad sincrónicas, los códigos auxiliares se pueden usar para concentrase únicamente en una unidad única de funcionalidad cada vez.
El código que usa temporizadores es problemático no solo porque las pruebas sean frágiles y lentas. Las pruebas de unidad se complican más cuando el código usa un temporizador que no se ejecuta en un subproceso de trabajador. En la biblioteca de clases de .NET, hay temporizadores diseñados de manera específica para usarlos en aplicaciones de UI, como System.Windows.Forms.Timer para Windows Forms o System.Windows.Threading.DispatcherTimer para aplicaciones de Windows Presentation Foundation (WPF) (vea “Comparación de las clases del temporizador en la biblioteca de clases de .NET Framework” en bit.ly/1r0SVic). Estos usan la cola de mensajes de la UI, que no está disponible directamente durante las pruebas de la unidad. La prueba mostrada al comienzo de este artículo no funcionará para estos temporizadores. La prueba tiene que poner en marcha el bombeo de mensajes, por ejemplo, mediante el DispatcherFrame de WPF (vea el ejemplo de la descarga del código). Para conservar las pruebas de unidad sencillas y claras cuando esté implementado temporizadores basados en UI, tiene que reemplazar estos temporizadores durante las pruebas. Estoy presentando una interfaz para que los temporizadores habiliten el reemplazo de los temporizadores “reales” con una implementación específicamente para pruebas. Hago esto para los temporizadores basados en “subprocesos” como System.Timers.Timer o System.Threading.Timer porque puedo mejorar entonces las pruebas de unidad en todos los casos. Se tiene que modificar el sistema que se está probando para usar esta interfaz de ITimer:
private readonly ITimer timer;
private readonly Object lockObject = new Object();
public void StartRecurring()
{
Message = "Init";
// Set up timer with 1 sec delay and 1 sec interval.
timer.StartTimer(() => { lock(lockObject){Message +=
" Poll";} }, new TimeSpan(0,0,0,1));
}
Al introducir la interfaz de ITimer, puedo reemplazar el comportamiento del temporizador durante las pruebas, como se muestra en la Figura 5.
Figura 5. Usar ITimer en SystemUnderTest
El esfuerzo adicional de definir el ITimer de la interfaz resulta rentable porque una prueba de unidad que comprueba el resultado de la inicialización y la operación periódica pueden ejecutarse ahora con mucha rapidez y de manera confiable en cuestión de milisegundos:
[Test]
public void VeryFastAndReliableTestWithTimer()
{
var dt = new DeterministicTimer();
var sut = new SystemUnderTest(dt);
// Execute code that sets up a timer 1 sec delay and 1 sec interval.
sut.StartRecurring();
// Tell timer that some time has elapsed.
dt.ElapseSeconds(3);
// Assert that outcome of three executions of the recurring operation is OK.
Assert.AreEqual("Init Poll Poll Poll", sut.Message);
}
El DeterministicTimer se escribe de manera específica para fines de pruebas. Permite a la prueba controlar el momento en que se ejecuta la acción del temporizador, sin esperar. La acción se ejecuta en el mismo subproceso que la propia prueba (vea la descarga del código para detalles de implementación de DeterministicTimer). Para la ejecución del código probado en un contexto que no es de pruebas, tengo que implementar un adaptador ITimer para un temporizador existente. La descarga del código contiene ejemplos de adaptadores para varios de los temporizadores de bibliotecas de clases de marco. La interfaz de ITimer puede adaptarse a las necesidades de la situación concreta y solo puede contener un subconjunto de la funcionalidad completa de temporizadores específicos.
Las pruebas de código asincrónico con un DeterministicTaskScheduler o un DeterministicTimer le permiten desactivar el multithreading fácilmente durante las pruebas. La funcionalidad se ejecuta en el mismo subproceso que la propia prueba. La interoperación del código de inicialización y del código asincrónico se conserva y se puede probar. Por ejemplo, una prueba de este tipo puede corregir los valores de tiempo correctos usados para inicializar un temporizador. Las excepciones se reenvían a las pruebas, de modo que pueden comprobar directamente el comportamiento del error del código.
Resumen
Las pruebas de la unidad efectivas del código asincrónico tienen tres ventajas principales: Se reducen los costos de mantenimiento para pruebas; las pruebas se ejecutan más rápido y se minimiza el riesgo de no ejecutar más las pruebas. Las soluciones presentadas en este artículo pueden ayudarle a lograr este objetivo.
La primera solución, la separación de la funcionalidad de los aspectos asincrónicos de un programa mediante un objeto humilde es la más genérica. Es aplicable para todas las situaciones, con independencia de cómo se inicien los subprocesos. Recomiendo usar esta solución para escenarios asincrónicos muy complejos, funcionalidad compleja o una combinación de ambos. Es un buen ejemplo del principio de diseño de la separación de conceptos (vea bit.ly/1lB8iHD).
La segunda solución, que sincroniza la prueba con la finalización del subproceso probado, se puede aplicar cuando el código que se está probando ofrece un mecanismo de sincronización como async y await. Esta solución tiene sentido cuando los requisitos previos del mecanismo de notificación se satisfacen de todos modos. De ser posible, use la sincronización async y await elegante cuando se inicien subprocesos que no sean de temporizador dado que las excepciones se propagan a la prueba. Las pruebas con temporizadores pueden usar await, eventos o llamadas a objetos dependientes. Estas pruebas pueden ser lentas cuando los temporizadores tengan intervalos o retrasos largos.
La tercera solución usa DeterministicTaskScheduler y el DeterministicTimer, evitando de esta manera la mayoría de las limitaciones e inconvenientes de las demás soluciones. La preparación del código que se está probando requiere algún esfuerzo pero se puede alcanzar una alta cobertura de código de prueba de unidad alta. Las pruebas de código con temporizadores se pueden ejecutar muy rápido sin la espera por retraso y tiempos de intervalo. Además, las excepciones se propagan a las pruebas. De modo que esta solución llevará a sólidos conjuntos de pruebas de unidad rápidos y elegantes combinados con alta cobertura de código.
Estas tres soluciones pueden ayudar a los desarrolladores de software a evitar los obstáculos del código asincrónico de pruebas de unidad. Se pueden usar para crear conjuntos de pruebas rápidos y eficaces y cubrir una amplia gama de técnicas de programación paralelas.
Sven Grand es un arquitecto de software de ingeniería de calidad para la unidad de negocios de rayos X de diagnóstico de Philips Healthcare. Se hizo adicto a las pruebas hace años, cuando oyó por primera vez acerca del desarrollo controlado por pruebas en una conferencia de software de Microsoft en 2001. Puede ponerse en contacto con él en la dirección de correo electrónico sven.grand@philips.com.
Gracias a los siguientes expertos técnicos por revisar este artículo: Stephen Cleary, James McCaffrey, Henning Pohl y Stephen Toub