multithreading - unitarias - ¿Cómo debo hacer una prueba unitaria de código enhebrado?
pruebas unitarias php (24)
¡Muy duro por cierto! En mis pruebas de unidad (C ++), he dividido esto en varias categorías a lo largo de las líneas del patrón de concurrencia utilizado:
Pruebas unitarias para clases que operan en un solo hilo y no son conscientes de hilos: fácil, pruebe como siempre.
Pruebas unitarias para los objetos Monitor (aquellos que ejecutan métodos sincronizados en el hilo de control de los llamantes) que exponen una API pública sincronizada: ejemplifican múltiples hilos simulados que ejercen la API. Construye escenarios que ejerciten condiciones internas del objeto pasivo. Incluya una prueba de ejecución más larga que, básicamente, supere a muchos de los subprocesos durante un largo período de tiempo. Esto no es científico, lo sé, pero sí genera confianza.
Pruebas unitarias para objetos activos (aquellas que encapsulan su propio hilo o hilos de control) - similares al # 2 anterior con variaciones según el diseño de la clase. La API pública puede estar bloqueando o no bloqueando, las personas que llaman pueden obtener futuros, los datos pueden llegar a las colas o deben ser retirados de la cola. Hay muchas combinaciones posibles aquí; caja blanca de distancia. Aún se requieren múltiples hilos simulados para hacer llamadas al objeto bajo prueba.
Como un aparte:
En la capacitación de desarrollador interno que hago, enseño los Pilares de la concurrencia y estos dos patrones como el marco principal para pensar y descomponer los problemas de concurrencia. Obviamente hay conceptos más avanzados pero he encontrado que este conjunto de conceptos básicos ayuda a mantener a los ingenieros fuera de la sopa. También conduce a un código que es más comprobable por unidad, como se describe anteriormente.
Hasta ahora he evitado la pesadilla que está probando el código de subprocesos múltiples, ya que parece un campo minado. Me gustaría preguntar cómo la gente ha probado el código que se basa en subprocesos para una ejecución exitosa, o simplemente cómo la gente ha probado este tipo de problemas que solo aparecen cuando dos subprocesos interactúan de una manera determinada.
Esto parece realmente un problema clave para los programadores de hoy, sería útil agrupar nuestro conocimiento en este caso.
(si es posible) no use hilos, use actores / objetos activos. Fácil de probar.
Echa un vistazo a mi respuesta relacionada en
Diseño de una clase de prueba para una barrera personalizada
Está sesgado hacia Java pero tiene un resumen razonable de las opciones.
Sin embargo, en resumen (IMO) no es el uso de un marco de trabajo elegante que garantice la corrección, sino la forma en que diseñará su código de multiproceso. Dividir las preocupaciones (concurrencia y funcionalidad) es un gran camino para aumentar la confianza. El software orientado a objetos en crecimiento guiado por pruebas explica algunas opciones mejor que yo.
El análisis estático y los métodos formales (ver, Concurrencia: Modelos de estado y programas Java ) son una opción, pero he encontrado que tienen un uso limitado en el desarrollo comercial.
No olvide que las pruebas de estilo de carga / remojo rara vez se garantizan para resaltar problemas.
¡Buena suerte!
El siguiente artículo sugiere 2 soluciones. Envolver un semáforo (CountDownLatch) y agrega funcionalidad como externalizar datos desde un hilo interno. Otra forma de lograr este propósito es usar Thread Pool (ver Puntos de interés).
Ha pasado un tiempo cuando se publicó esta pregunta, pero aún no se responde ...
La respuesta de kleolb02 es buena. Intentaré entrar en más detalles.
Hay una manera, que practico para el código C #. Para las pruebas unitarias, debería poder programar pruebas reproducibles , que es el mayor desafío en el código de multiproceso. Así que mi respuesta apunta a forzar el código asíncrono en un arnés de prueba, que funciona de manera síncrona .
Es una idea del libro " xUnit Test Patterns " de Gerard Meszardos y se llama "Humble Object" (p. 695): tienes que separar el código de la lógica central y todo lo que huele a código asíncrono. Esto daría lugar a una clase para la lógica central, que funciona de forma síncrona .
Esto lo coloca en la posición de probar el código lógico del núcleo de manera sincrónica . Usted tiene el control absoluto sobre el tiempo de las llamadas que está haciendo en la lógica central y, por lo tanto, puede realizar pruebas reproducibles . Y esta es su ganancia al separar la lógica central y la lógica asíncrona.
Esta lógica central debe estar rodeada por otra clase, que es responsable de recibir llamadas a la lógica central de forma asíncrona y delega estas llamadas a la lógica central. El código de producción solo accederá a la lógica central a través de esa clase. Debido a que esta clase solo debe delegar llamadas, es una clase muy "tonta" sin mucha lógica. Así que puedes mantener tus pruebas de unidad para esta clase trabajadora asíncrona al mínimo.
Cualquier cosa por encima de eso (prueba de interacción entre clases) son pruebas de componentes. También en este caso, debería poder tener un control absoluto sobre el tiempo, si se adhiere al patrón de "Objeto humilde".
Hace poco descubrí (para Java) una herramienta llamada Threadsafe. Es una herramienta de análisis estático muy similar a los errores de búsqueda pero específicamente para detectar problemas de subprocesos múltiples. No es un reemplazo para las pruebas, pero puedo recomendarlo como parte de la escritura de Java multihebra confiable.
Incluso atrapa algunos problemas potenciales muy sutiles en torno a cosas como la subsunción de clases, el acceso a objetos inseguros a través de clases concurrentes y la detección de modificadores volátiles faltantes cuando se usa el paradigma de bloqueo de doble verificación.
Si escribes Java multiproceso contemplateltd.com/threadsafe
Hay algunas herramientas que son bastante buenas. Aquí hay un resumen de algunos de los de Java.
Algunas buenas herramientas de análisis estático incluyen FindBugs (proporciona algunas sugerencias útiles), JLint , Java Pathfinder (JPF y JPF2) y Bogor .
MultithreadedTC es una herramienta de análisis dinámico bastante buena (integrada en JUnit) donde tienes que configurar tus propios casos de prueba.
ConTest de IBM Research es interesante. Instruye tu código insertando todo tipo de comportamientos de modificación de subprocesos (p. Ej., Sueño y rendimiento) para tratar de descubrir errores al azar.
SPIN es una herramienta realmente genial para modelar sus componentes Java (y otros), pero necesita tener un marco útil. Es difícil de usar tal como está, pero extremadamente poderoso si sabes cómo usarlo. Bastantes herramientas usan SPIN debajo del capó.
MultithreadedTC es probablemente la más común, pero definitivamente vale la pena ver algunas de las herramientas de análisis estático mencionadas anteriormente.
He enfrentado este problema varias veces en los últimos años al escribir código de manejo de hilos para varios proyectos. Estoy proporcionando una respuesta tardía porque la mayoría de las otras respuestas, aunque ofrecen alternativas, en realidad no responden la pregunta sobre las pruebas. Mi respuesta está dirigida a los casos en los que no hay alternativa al código multihilo; Cubro los problemas de diseño de código para completar, pero también discuto las pruebas de unidad.
Escritura de código multihilo comprobable
Lo primero que debe hacer es separar su código de manejo de subprocesos de producción de todo el código que realiza el procesamiento de datos real. De esa manera, el procesamiento de datos se puede probar como código de subproceso único, y lo único que hace el código multiproceso es coordinar los subprocesos.
Lo segundo que hay que recordar es que los errores en el código multiproceso son probabilísticos; los errores que se manifiestan con menor frecuencia son los errores que se colarán en la producción, serán difíciles de reproducir incluso en la producción y, por lo tanto, causarán los mayores problemas. Por esta razón, el enfoque de codificación estándar de escribir el código rápidamente y luego depurarlo hasta que funcione es una mala idea para el código multiproceso; resultará en un código donde se solucionan los errores fáciles y los errores peligrosos todavía están allí.
En su lugar, al escribir código de multiproceso, debe escribir el código con la actitud de que va a evitar escribir los errores en primer lugar. Si ha eliminado correctamente el código de procesamiento de datos, el código de manejo de subprocesos debería ser lo suficientemente pequeño, preferiblemente unas pocas líneas, en el peor de una docena de líneas, para que tenga la oportunidad de escribirlo sin escribir un error, y ciertamente sin escribir muchos errores. , si entiendes el enhebrado, tómate tu tiempo y ten cuidado.
Escritura de pruebas unitarias para código multiproceso.
Una vez que el código multiproceso se escribe lo más cuidadosamente posible, todavía vale la pena escribir pruebas para ese código. El objetivo principal de las pruebas no es tanto probar los errores de condición de carrera que dependen en gran medida del tiempo, es imposible probar repetidamente tales condiciones de carrera, sino probar que su estrategia de bloqueo para prevenir dichos errores permite que varios hilos interactúen según lo previsto .
Para probar correctamente el comportamiento de bloqueo correcto, una prueba debe iniciar varios subprocesos. Para que la prueba sea repetible, queremos que las interacciones entre los hilos se realicen en un orden predecible. No queremos sincronizar externamente los subprocesos en la prueba, porque eso enmascarará los errores que podrían ocurrir en la producción donde los subprocesos no están sincronizados externamente. Eso deja el uso de retardos de tiempo para la sincronización de subprocesos, que es la técnica que he usado con éxito cada vez que he tenido que escribir pruebas de código de multiproceso.
Si los retrasos son demasiado cortos, entonces la prueba se vuelve frágil, ya que las pequeñas diferencias de tiempo, por ejemplo, entre diferentes máquinas en las que se pueden ejecutar las pruebas, pueden hacer que el tiempo se desactive y la prueba falle. Lo que normalmente hago es comenzar con demoras que causan fallas en las pruebas, aumentar las demoras para que la prueba pase de manera confiable en mi máquina de desarrollo y luego duplicar las demoras más allá de eso para que la prueba tenga una buena probabilidad de pasar a otras máquinas. Esto significa que la prueba tomará una cantidad macroscópica de tiempo, aunque, según mi experiencia, un diseño de prueba cuidadoso puede limitar ese tiempo a no más de una docena de segundos. Como no debería tener muchos lugares que requieran código de coordinación de subprocesos en su aplicación, eso debería ser aceptable para su conjunto de pruebas.
Finalmente, mantenga un registro de la cantidad de errores detectados por su prueba. Si su prueba tiene una cobertura de código del 80%, se puede esperar que detecte alrededor del 80% de sus errores. Si su prueba está bien diseñada pero no encuentra errores, existe una posibilidad razonable de que no tenga errores adicionales que solo aparezcan en producción. Si la prueba detecta uno o dos errores, es posible que aún tengas suerte. Más allá de eso, es posible que desee considerar una revisión cuidadosa o incluso una reescritura completa de su código de manejo de subprocesos, ya que es probable que el código aún contenga errores ocultos que serán muy difíciles de encontrar hasta que el código esté en producción, y muy Difícil de arreglar entonces.
He hecho mucho de esto, y sí, apesta.
Algunos consejos:
- GroboUtils para ejecutar múltiples hilos de prueba
- alphaWorks ConTest para instrumentar clases para hacer que los entrecruzamientos varíen entre iteraciones
- Cree un campo
throwable
ytearDown
entearDown
(vea el Listado 1). Si detectas una excepción errónea en otro hilo, simplemente asignalo a lanzar. - Creé la clase de utils en el Listado 2 y la encontré de un valor incalculable, especialmente waitForVerify y waitForCondition, lo que aumentará considerablemente el rendimiento de sus pruebas.
- Haz buen uso de
AtomicBoolean
en tus pruebas. Es seguro para subprocesos y, a menudo, necesitará un tipo de referencia final para almacenar valores de clases de devolución de llamada y similares. Ver ejemplo en el Listado 3. - Asegúrese de dar siempre un tiempo de espera a su prueba (p. Ej.,
@Test(timeout=60*1000)
), ya que las pruebas de concurrencia a veces se pueden bloquear para siempre cuando se interrumpen
Listado 1:
@After
public void tearDown() {
if ( throwable != null )
throw throwable;
}
Listado 2:
import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;
import ca.digitalrapids.io.DRFileUtils;
/**
* Various utilities for testing
*/
public abstract class DRTestUtils
{
static private Random random = new Random();
/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
* default max wait and check period values.
*/
static public void waitForCondition(Predicate predicate, String errorMessage)
throws Throwable
{
waitForCondition(null, null, predicate, errorMessage);
}
/** Blocks until a condition is true, throwing an {@link AssertionError} if
* it does not become true during a given max time.
* @param maxWait_ms max time to wait for true condition. Optional; defaults
* to 30 * 1000 ms (30 seconds).
* @param checkPeriod_ms period at which to try the condition. Optional; defaults
* to 100 ms.
* @param predicate the condition
* @param errorMessage message use in the {@link AssertionError}
* @throws Throwable on {@link AssertionError} or any other exception/error
*/
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms,
Predicate predicate, String errorMessage) throws Throwable
{
waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
public void execute(Object errorMessage)
{
fail((String)errorMessage);
}
}, errorMessage);
}
/** Blocks until a condition is true, running a closure if
* it does not become true during a given max time.
* @param maxWait_ms max time to wait for true condition. Optional; defaults
* to 30 * 1000 ms (30 seconds).
* @param checkPeriod_ms period at which to try the condition. Optional; defaults
* to 100 ms.
* @param predicate the condition
* @param closure closure to run
* @param argument argument for closure
* @throws Throwable on {@link AssertionError} or any other exception/error
*/
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms,
Predicate predicate, Closure closure, Object argument) throws Throwable
{
if ( maxWait_ms == null )
maxWait_ms = 30 * 1000;
if ( checkPeriod_ms == null )
checkPeriod_ms = 100;
StopWatch stopWatch = new StopWatch();
stopWatch.start();
while ( !predicate.evaluate(null) ) {
Thread.sleep(checkPeriod_ms);
if ( stopWatch.getTime() > maxWait_ms ) {
closure.execute(argument);
}
}
}
/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
* for {@code maxWait_ms}
*/
static public void waitForVerify(Object easyMockProxy)
throws Throwable
{
waitForVerify(null, easyMockProxy);
}
/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
* max wait time has elapsed.
* @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
* @param easyMockProxy Proxy to call verify on
* @throws Throwable
*/
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
throws Throwable
{
if ( maxWait_ms == null )
maxWait_ms = 30 * 1000;
StopWatch stopWatch = new StopWatch();
stopWatch.start();
for(;;) {
try
{
verify(easyMockProxy);
break;
}
catch (AssertionError e)
{
if ( stopWatch.getTime() > maxWait_ms )
throw e;
Thread.sleep(100);
}
}
}
/** Returns a path to a directory in the temp dir with the name of the given
* class. This is useful for temporary test files.
* @param aClass test class for which to create dir
* @return the path
*/
static public String getTestDirPathForTestClass(Object object)
{
String filename = object instanceof Class ?
((Class)object).getName() :
object.getClass().getName();
return DRFileUtils.getTempDir() + File.separator +
filename;
}
static public byte[] createRandomByteArray(int bytesLength)
{
byte[] sourceBytes = new byte[bytesLength];
random.nextBytes(sourceBytes);
return sourceBytes;
}
/** Returns <code>true</code> if the given object is an EasyMock mock object
*/
static public boolean isEasyMockMock(Object object) {
try {
InvocationHandler invocationHandler = Proxy
.getInvocationHandler(object);
return invocationHandler.getClass().getName().contains("easymock");
} catch (IllegalArgumentException e) {
return false;
}
}
}
Listado 3:
@Test
public void testSomething() {
final AtomicBoolean called = new AtomicBoolean(false);
subject.setCallback(new SomeCallback() {
public void callback(Object arg) {
// check arg here
called.set(true);
}
});
subject.run();
assertTrue(called.get());
}
He tenido la desafortunada tarea de probar el código de subprocesos y definitivamente son las pruebas más difíciles que he escrito.
Al escribir mis pruebas, usé una combinación de delegados y eventos. Básicamente, se trata de usar eventos PropertyNotifyChanged
con WaitCallback
o algún tipo de ConditionalWaiter
que sondea.
No estoy seguro de si este fue el mejor enfoque, pero ha funcionado para mí.
La concurrencia es una interacción compleja entre el modelo de memoria, el hardware, los cachés y nuestro código. En el caso de Java, al menos tales pruebas han sido abordadas principalmente por jcstress . Se sabe que los creadores de esa biblioteca son autores de muchas características de concurrencia de JVM, GC y Java.
Pero incluso esta biblioteca necesita un buen conocimiento de la especificación del modelo de memoria Java para que sepamos exactamente lo que estamos probando. Pero creo que el foco de este esfuerzo es mircobenchmarks. No grandes aplicaciones de negocios.
Me gusta escribir dos o más métodos de prueba para ejecutar en hilos paralelos, y cada uno de ellos realiza llamadas al objeto bajo prueba. He estado usando las llamadas de reposo () para coordinar el orden de las llamadas de los distintos subprocesos, pero eso no es realmente confiable. También es mucho más lento porque tienes que dormir lo suficiente como para que el tiempo funcione normalmente.
Encontré la this del mismo grupo que escribió FindBugs. Te permite especificar el orden de los eventos sin usar Sleep (), y es confiable. No lo he probado todavía.
La mayor limitación de este enfoque es que solo le permite probar los escenarios que sospecha que causarán problemas. Como han dicho otros, realmente necesita aislar su código de multiproceso en una pequeña cantidad de clases simples para tener alguna esperanza de probarlas a fondo.
Una vez que haya probado cuidadosamente los escenarios que espera causar problemas, una prueba no científica que arroja un montón de solicitudes simultáneas a la clase por un tiempo es una buena manera de buscar problemas inesperados.
Actualización: He jugado un poco con la biblioteca Java de multiproceso TC, y funciona bien. También he portado algunas de sus características a una versión .NET que llamo TickingTest .
Mira, no hay una manera fácil de hacer esto. Estoy trabajando en un proyecto que es intrínsecamente multihilo. Los eventos vienen del sistema operativo y tengo que procesarlos al mismo tiempo.
La forma más sencilla de lidiar con las pruebas de código de aplicación complejo y multiproceso es la siguiente: si es demasiado complejo de probar, lo está haciendo mal. Si tiene una sola instancia que tiene múltiples subprocesos que actúan sobre ella y no puede probar situaciones en las que estos subprocesos se cruzan entre sí, entonces su diseño debe volver a realizarse. Es tan simple y tan complejo como esto.
Hay muchas formas de programar para multiproceso que evita que los subprocesos se ejecuten a través de instancias al mismo tiempo. Lo más sencillo es hacer que todos tus objetos sean inmutables. Por supuesto, eso no suele ser posible. Por lo tanto, tiene que identificar aquellos lugares en su diseño donde los hilos interactúan con la misma instancia y reducir el número de esos lugares. Al hacer esto, se aíslan algunas clases en las que realmente se produce multihilo, lo que reduce la complejidad general de las pruebas de su sistema.
Pero debes darte cuenta de que, incluso al hacer esto, no puedes probar cada situación en la que dos hilos se unen entre sí. Para hacer eso, tendrías que ejecutar dos subprocesos simultáneamente en la misma prueba, luego controlar exactamente qué líneas están ejecutando en un momento dado. Lo mejor que puedes hacer es simular esta situación. Pero esto podría requerir que usted codifique específicamente para las pruebas, y eso es, en el mejor de los casos, medio paso hacia una verdadera solución.
Probablemente la mejor manera de probar el código para problemas de subprocesos es a través del análisis estático del código. Si su código enlazado no sigue un conjunto finito de patrones de seguridad de hilos, es posible que tenga un problema. Creo que el análisis de código en VS contiene cierto conocimiento de subprocesos, pero probablemente no mucho.
Mire, tal como están las cosas en la actualidad (y probablemente se espera que llegue un buen momento), la mejor manera de probar aplicaciones de multiproceso es reducir la complejidad del código de subprocesos tanto como sea posible. Minimice las áreas donde los hilos interactúan, pruebe lo mejor posible y use el análisis de código para identificar las áreas de peligro.
Otra forma de (un poco) probar el código de subprocesos y los sistemas muy complejos en general es a través de Fuzz Testing . No es genial, y no encontrará todo, pero es probable que sea útil y simple de hacer.
Citar:
Fuzz testing o fuzzing es una técnica de prueba de software que proporciona datos aleatorios ("fuzz") a las entradas de un programa. Si el programa falla (por ejemplo, al bloquearse o al fallar las aserciones del código incorporado), se pueden observar los defectos. La gran ventaja de las pruebas fuzz es que el diseño de la prueba es extremadamente simple y libre de ideas preconcebidas sobre el comportamiento del sistema.
...
Las pruebas Fuzz se usan a menudo en grandes proyectos de desarrollo de software que emplean pruebas de caja negra. Estos proyectos generalmente tienen un presupuesto para desarrollar herramientas de prueba, y la prueba de fuzz es una de las técnicas que ofrece una alta relación beneficio-costo.
...
Sin embargo, las pruebas fuzz no son un sustituto de las pruebas exhaustivas o los métodos formales: solo puede proporcionar una muestra aleatoria del comportamiento del sistema y, en muchos casos, pasar una prueba fuzz solo puede demostrar que un software maneja las excepciones sin fallar, en lugar de comportándose correctamente. Por lo tanto, las pruebas fuzz solo pueden considerarse como una herramienta de búsqueda de errores en lugar de una garantía de calidad.
Para Java, consulte el capítulo 12 de JCIP . Hay algunos ejemplos concretos de escritura de pruebas unitarias deterministas y de múltiples hilos para al menos probar la corrección e invariantes del código concurrente.
"Probar" la seguridad del hilo con pruebas unitarias es mucho más complejo. Mi opinión es que esto se puede realizar mejor mediante pruebas de integración automatizadas en una variedad de plataformas / configuraciones.
Para el código J2E, he usado SilkPerformer, LoadRunner y JMeter para pruebas de concurrencia de subprocesos. Todos hacen lo mismo. Básicamente, le brindan una interfaz relativamente simple para administrar su versión del servidor proxy, requerida, para analizar el flujo de datos TCP / IP y simular múltiples usuarios que realizan solicitudes simultáneas a su servidor de aplicaciones. El servidor proxy puede darle la posibilidad de hacer cosas como analizar las solicitudes realizadas, presentando la página completa y la URL enviada al servidor, así como la respuesta del servidor, después de procesar la solicitud.
Puede encontrar algunos errores en el modo http inseguro, donde al menos puede analizar los datos del formulario que se está enviando y alterarlos sistemáticamente para cada usuario. Pero las pruebas verdaderas son cuando se ejecuta en https (Capas de sockets seguras). Luego, también debe lidiar con la alteración sistemática de la sesión y los datos de las cookies, que pueden ser un poco más complicados.
El mejor error que encontré, mientras probaba la concurrencia, fue cuando descubrí que el desarrollador había confiado en la recolección de elementos no utilizados de Java para cerrar la solicitud de conexión que se estableció al iniciar sesión, al servidor LDAP, al iniciar sesión. a las sesiones de otros usuarios y resultados muy confusos, cuando se trata de analizar lo que sucedió cuando el servidor fue puesto de rodillas, apenas capaz de completar una transacción, cada pocos segundos.
Al final, usted o alguien probablemente tendrá que abrocharse el cinturón y analizar el código en busca de errores como el que acabo de mencionar. Y una discusión abierta entre departamentos, como la que ocurrió, cuando se desarrolló el problema descrito anteriormente, son las más útiles. Pero estas herramientas son la mejor solución para probar código de subprocesos múltiples. JMeter es de código abierto. SilkPerformer y LoadRunner son propietarios. Si realmente quieres saber si tu aplicación es segura para subprocesos, así es como lo hacen los grandes. He hecho esto para empresas muy grandes profesionalmente, así que no estoy adivinando. Estoy hablando por experiencia personal.
Una advertencia: lleva tiempo entender estas herramientas. No será cuestión de simplemente instalar el software y activar la GUI, a menos que ya haya tenido cierta exposición a la programación de subprocesos múltiples. He intentado identificar las 3 categorías críticas de áreas a comprender (formularios, datos de sesión y cookies), con la esperanza de que al menos comenzar con la comprensión de estos temas lo ayudará a concentrarse en resultados rápidos, en lugar de tener que leer los toda la documentación.
Pasé la mayor parte de la semana pasada en una biblioteca universitaria estudiando la depuración de códigos concurrentes. El problema central es que el código concurrente no es determinista. Por lo general, la depuración académica ha caído en uno de los tres campos aquí:
- Evento de seguimiento / reproducción. Esto requiere un monitor de eventos y luego revisar los eventos que se enviaron. En un marco de UT, esto implicaría enviar manualmente los eventos como parte de una prueba y luego realizar revisiones post mortem.
- Scriptable Aquí es donde interactúas con el código en ejecución con un conjunto de disparadores. "En x> foo, baz ()". Esto podría interpretarse en un marco UT en el que tiene un sistema en tiempo de ejecución que activa una prueba determinada en una determinada condición.
- Interactivo. Esto obviamente no funcionará en una situación de prueba automática. ;)
Ahora, como los comentaristas anteriores han notado, puede diseñar su sistema concurrente en un estado más determinista. Sin embargo, si no lo hace correctamente, simplemente está volviendo a diseñar un sistema secuencial nuevamente.
Mi sugerencia sería centrarse en tener un protocolo de diseño muy estricto sobre qué se enlaza y qué no se enlaza. Si restringe su interfaz para que haya una mínima dependencia entre los elementos, es mucho más fácil.
Buena suerte, y sigue trabajando en el problema.
Probar el código MT para verificar su corrección es, como ya se dijo, un problema difícil. Al final, se reduce a garantizar que no haya carreras de datos sincronizadas incorrectamente en su código. El problema con esto es que hay infinitas posibilidades de ejecución de subprocesos (interleavings) sobre las cuales no tienes mucho control (asegúrate de leer this artículo, sin embargo). En escenarios simples, podría ser posible probar la exactitud mediante el razonamiento, pero este no suele ser el caso. Especialmente si desea evitar / minimizar la sincronización y no optar por la opción de sincronización más obvia / más fácil.
Un enfoque que sigo es escribir un código de prueba altamente concurrente para hacer que puedan ocurrir carreras de datos potencialmente no detectadas. Y luego realizo esas pruebas por algún tiempo :) Una vez me topé con una charla en la que un científico informático mostraba una herramienta que hace esto (diseñando pruebas a partir de especificaciones al azar y luego ejecutándolas de forma desenfrenada, al mismo tiempo, comprobando las invariantes definidas). Estar destrozado).
Por cierto, creo que este aspecto de las pruebas de código MT no se ha mencionado aquí: identifique invariantes del código que puede verificar aleatoriamente. Desafortunadamente, encontrar a esos invariantes también es un problema difícil. Además, es posible que no se mantengan todo el tiempo durante la ejecución, por lo que debe encontrar / imponer puntos de ejecución en los que pueda esperar que sean ciertos. Llevar la ejecución del código a tal estado también es un problema difícil (y podría incurrir en problemas de concurrencia. ¡Menos mal, es muy difícil!
Algunos enlaces interesantes para leer:
- this : un marco que permite forzar ciertas interconexiones de hilos y luego verificar si hay invariantes.
- jMock Blitzer : sincronización de prueba de esfuerzo
- assertConcurrent : versión JUnit de la sincronización de pruebas de estrés
- Código de prueba concurrente : breve descripción de los dos métodos principales de fuerza bruta (prueba de esfuerzo) o determinista (para los invariantes)
Puede usar EasyMock.makeThreadSafe para hacer que la instancia de prueba sea segura para subprocesos
Si estás probando Thread nuevo (ejecutable) .run (), entonces puedes simular un Thread para ejecutar el ejecutable secuencialmente
Por ejemplo, si el código del objeto probado invoca un nuevo hilo como este
Class TestedClass {
public void doAsychOp() {
new Thread(new myRunnable()).start();
}
}
Luego, burlándose de nuevos subprocesos y ejecutar el argumento ejecutable de forma secuencial puede ayudar
@Mock
private Thread threadMock;
@Test
public void myTest() throws Exception {
PowerMockito.mockStatic(Thread.class);
//when new thread is created execute runnable immediately
PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
@Override
public Thread answer(InvocationOnMock invocation) throws Throwable {
// immediately run the runnable
Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
if(runnable != null) {
runnable.run();
}
return threadMock;//return a mock so Thread.start() will do nothing
}
});
TestedClass testcls = new TestedClass()
testcls.doAsychOp(); //will invoke myRunnable.run in current thread
//.... check expected
}
También tuve serios problemas al probar código multihilo. Luego encontré una solución realmente genial en "xUnit Test Patterns" de Gerard Meszaros. El patrón que describe se llama objeto humilde .
Básicamente, describe cómo se puede extraer la lógica en un componente separado y fácil de probar que está desacoplado de su entorno. Después de probar esta lógica, puede probar el comportamiento complicado (multihilo, ejecución asíncrona, etc.)
Yo manejo las pruebas unitarias de componentes roscados de la misma manera que yo manejo cualquier prueba unitaria, es decir, con inversión de marcos de control y aislamiento. Me desarrollo en el .Net-arena y fuera de la caja el enhebrado (entre otras cosas) es muy difícil (diría casi imposible) de aislar completamente.
Por lo tanto, he escrito envoltorios que se parecen a esto (simplificado):
public interface IThread
{
void Start();
...
}
public class ThreadWrapper : IThread
{
private readonly Thread _thread;
public ThreadWrapper(ThreadStart threadStart)
{
_thread = new Thread(threadStart);
}
public Start()
{
_thread.Start();
}
}
public interface IThreadingManager
{
IThread CreateThread(ThreadStart threadStart);
}
public class ThreadingManager : IThreadingManager
{
public IThread CreateThread(ThreadStart threadStart)
{
return new ThreadWrapper(threadStart)
}
}
Desde allí puedo inyectar fácilmente el IThreadingManager en mis componentes y usar el marco de aislamiento de mi elección para hacer que el hilo se comporte como espero durante la prueba.
Hasta ahora me ha funcionado muy bien, y utilizo el mismo enfoque para el grupo de subprocesos, las cosas en System.Environment, Sleep, etc.
Awaitility también puede ser útil para ayudarlo a escribir pruebas unitarias deterministas. Le permite esperar hasta que se actualice algún estado de su sistema. Por ejemplo:
await().untilCall( to(myService).myMethod(), greaterThan(3) );
o
await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));
También tiene soporte Scala y Groovy.
await until { something() > 4 } // Scala example
Pete Goodliffe tiene una serie sobre las pruebas unitarias de código roscado .
Es dificil. Tomo la salida más fácil y trato de mantener el código de subproceso abstraído de la prueba real. Pete menciona que la forma en que lo hago está mal, pero o tengo la separación correcta o simplemente he tenido suerte.