¿Cómo debo probar el código roscado de la unidad?

704

Hasta ahora he evitado la pesadilla que está probando el código de subprocesos múltiples, ya que parece demasiado campo de minas. Me gustaría preguntar cómo la gente ha probado el código que se basa en subprocesos para una ejecución exitosa, o cómo las personas han intentado probar ese tipo de problemas que solo aparecen cuando dos subprocesos interactúan de una manera determinada.

Este parece ser un problema realmente clave para los programadores de hoy, sería útil aunar nuestros conocimientos en este caso.

jkp
fuente
2
Estaba pensando en publicar una pregunta sobre este mismo problema. Si bien Will hace muchos puntos buenos a continuación, creo que podemos hacerlo mejor. Estoy de acuerdo en que no hay un "enfoque" único para tratar esto de manera limpia. Sin embargo, "probar lo mejor que pueda" está poniendo el listón muy bajo. Volveré con mis hallazgos.
Zach Burlingame
En Java: el paquete java.util.concurrent contiene algunas clases mal conocidas, que pueden ayudar a escribir pruebas JUnit deterministas. Echa un vistazo a - CountDownLatch - Semaphore - Intercambiador
Synox
¿Puede proporcionar un enlace a su pregunta anterior relacionada con la prueba de la unidad, por favor?
Andrew Grimm
77
Creo que es importante tener en cuenta que esta pregunta tiene 8 años, y las bibliotecas de aplicaciones han recorrido un largo camino mientras tanto. En la "era moderna" (2016), el desarrollo de subprocesos múltiples surge principalmente en sistemas embebidos. Pero si está trabajando en una aplicación de escritorio o teléfono, primero explore las alternativas. Los entornos de aplicaciones como .NET ahora incluyen herramientas para administrar o simplificar en gran medida probablemente el 90% de los escenarios comunes de subprocesos múltiples. (asnync / await, PLinq, IObservable, el TPL ...). El código multiproceso es difícil. Si no reinventa la rueda, no tiene que volver a probarla.
Paul Williams

Respuestas:

245

Mira, no hay una manera fácil de hacer esto. Estoy trabajando en un proyecto que es inherentemente multiproceso. Los eventos provienen del sistema operativo y tengo que procesarlos simultáneamente.

La forma más sencilla de lidiar con la prueba de código de aplicación complejo y multiproceso es esta: si es demasiado complejo para probarlo, lo está haciendo mal. Si tiene una única instancia que tiene varios subprocesos que actúan sobre ella, y no puede probar situaciones en las que estos subprocesos se superponen, entonces su diseño debe rehacerse. Es tan simple y tan complejo como esto.

Hay muchas formas de programar para subprocesos múltiples que evitan que los subprocesos se ejecuten a través de instancias al mismo tiempo. Lo más simple es hacer que todos sus objetos sean inmutables. Por supuesto, eso no suele ser posible. Por lo tanto, debe identificar esos 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, aísla algunas clases en las que realmente se produce el subprocesamiento múltiple, lo que reduce la complejidad general de probar su sistema.

Pero debes darte cuenta de que, incluso al hacer esto, aún no puedes probar todas las situaciones en las que dos hilos se cruzan. Para hacer eso, tendría que ejecutar dos hilos 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 puede requerir que codifique específicamente para las pruebas, y eso es, en el mejor de los casos, medio paso hacia una solución verdadera.

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 enhebrado no sigue un conjunto finito de patrones seguros para subprocesos, entonces podría tener un problema. Creo que el Análisis de código en VS contiene cierto conocimiento de subprocesos, pero probablemente no mucho.

Mire, como están las cosas actualmente (y probablemente lo harán en un buen momento), la mejor manera de probar aplicaciones multiproceso es reducir la complejidad del código roscado 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 áreas peligrosas.

undur_gongor
fuente
1
El análisis de código es excelente si maneja un lenguaje / marco que lo permita. EG: Findbugs encontrará problemas de concurrencia compartida muy simples y fáciles con variables estáticas. Lo que no puede encontrar son patrones de diseño singleton, se supone que todos los objetos se pueden crear varias veces. Este complemento es lamentablemente inadecuado para marcos como Spring.
Zombis
3
en realidad hay una cura: objetos activos. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…
Dill
66
Si bien este es un buen consejo, todavía me pregunto, "¿cómo pruebo esas áreas mínimas donde se requieren múltiples hilos?"
Bryan Rayner
55
"Si es demasiado complejo para probar, lo estás haciendo mal", todos tenemos que sumergirnos en el código heredado que no escribimos. ¿Cómo ayuda esta observación a alguien exactamente?
Ronna
2
El análisis estático es probablemente una buena idea, pero no está probando. Esta publicación realmente no responde la pregunta, que trata sobre cómo realizar la prueba.
Warren Dew
96

Ha pasado un tiempo cuando se publicó esta pregunta, pero aún no se ha respondido ...

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, debe poder programar pruebas reproducibles , que es el mayor desafío en el código multiproceso. Entonces, mi respuesta apunta a forzar el código asíncrono en un arnés de prueba, que funciona de forma sincrónica .

Es una idea del libro de Gerard Meszardos " Patrones de prueba xUnit " y se llama "Objeto humilde" (p. 695): debe separar el código lógico central y todo lo que huela a código asíncrono entre sí. Esto daría como resultado una clase para la lógica central, que funciona sincrónicamente .

Esto lo coloca en la posición de probar el código lógico central de forma síncrona . Usted tiene 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 asincrónica.

Esta lógica central necesita ser ajustada por otra clase, que es responsable de recibir llamadas a la lógica central de forma asincrónica 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 debería delegar llamadas, es una clase muy "tonta" sin mucha lógica. Por lo tanto, puede mantener sus pruebas unitarias para esta clase de trabajo asíncrona como 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 "Objeto Humilde".

Theo Lenndorff
fuente
1
Pero a veces, si los hilos cooperan bien entre sí, también hay algo que debe probarse, ¿verdad? Definitivamente, separaré la lógica central de la parte asíncrona después de leer su respuesta. Pero todavía voy a probar la lógica a través de interfaces asíncronas con una devolución de llamada de trabajo en todos los hilos.
CopperCash el
¿Qué pasa con los sistemas multiprocesador?
Technophile
65

Duro uno de hecho! En mis pruebas unitarias (C ++), he dividido esto en varias categorías a lo largo de las líneas del patrón de concurrencia utilizado:

  1. Pruebas unitarias para clases que operan en un solo subproceso y no son compatibles con subprocesos: fácil, pruebe como de costumbre.

  2. Pruebas unitarias para objetos de Monitor (aquellos que ejecutan métodos sincronizados en el hilo de control de las personas que llaman) que exponen una API pública sincronizada - instancian múltiples hilos simulados que ejercen la API. Construir escenarios que ejerzan condiciones internas del objeto pasivo. Incluya una prueba de ejecución más larga que básicamente lo supere en múltiples subprocesos durante un largo período de tiempo. Esto no es científico, lo sé, pero genera confianza.

  3. Pruebas unitarias para objetos activos (aquellos que encapsulan su propio hilo o hilos de control) - similar al # 2 anterior con variaciones dependiendo del 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 retirarse. Hay muchas combinaciones posibles aquí; caja blanca de distancia. Todavía requiere múltiples hilos simulados para hacer llamadas al objeto bajo prueba.

Como un aparte:

En la capacitación interna para desarrolladores que hago, enseño los Pilares de la concurrencia y estos dos patrones como marco principal para pensar y descomponer los problemas de concurrencia. Obviamente, existen conceptos más avanzados, pero he descubierto 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 describió anteriormente.

David Joyner
fuente
51

Me he enfrentado a este problema varias veces en los últimos años al escribir código de manejo de hilos para varios proyectos. Proporciono 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 se dirige a los casos donde no hay alternativa al código multiproceso; Cubro los problemas de diseño del código para completarlos, pero también analizo las pruebas unitarias.

Escribir código multiproceso 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 subprocesos.

La segunda cosa para recordar es que los errores en el código multiproceso son probabilísticos; Los errores que se manifiestan con menos frecuencia son los errores que se infiltrará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; dará como resultado un código donde se arreglan los errores fáciles y los errores peligrosos todavía están allí.

En cambio, al escribir código 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 debe ser lo suficientemente pequeño, preferiblemente unas pocas líneas, en el peor de los casos, unas pocas docenas de líneas, de modo que tenga la posibilidad de escribirlo sin escribir un error, y ciertamente sin escribir muchos errores , si comprende el enhebrado, tómese su tiempo y tenga cuidado.

Escritura de pruebas unitarias para código multiproceso

Una vez que el código multiproceso se escribe con el mayor cuidado posible, todavía vale la pena escribir pruebas para ese código. El propósito principal de las pruebas no es tanto para detectar errores de condición de carrera altamente dependientes del tiempo, es imposible probar repetidamente tales condiciones de carrera, sino probar que su estrategia de bloqueo para prevenir tales errores permite que múltiples hilos interactúen según lo previsto .

Para probar correctamente el comportamiento correcto de bloqueo, una prueba debe iniciar varios subprocesos. Para que la prueba sea repetible, queremos que las interacciones entre los hilos sucedan en un orden predecible. No queremos sincronizar externamente los hilos en la prueba, porque eso enmascarará los errores que podrían ocurrir en la producción donde los hilos no están sincronizados externamente. Eso deja el uso de retrasos de sincronización para la sincronización de subprocesos, que es la técnica que he utilizado con éxito cada vez que tuve que escribir pruebas de código multiproceso.

Si los retrasos son demasiado cortos, entonces la prueba se vuelve frágil, porque 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 generalmente he hecho es comenzar con demoras que causan fallas en la prueba, 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 posibilidad de pasar en otras máquinas. Esto significa que la prueba tomará una cantidad de tiempo macroscópica, aunque en mi experiencia, el diseño cuidadoso de la prueba puede limitar ese tiempo a no más de una docena de segundos. Dado que 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, realice un seguimiento de la cantidad de errores detectados por su prueba. Si su prueba tiene una cobertura de código del 80%, se espera 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 aparecerán en la producción. Si la prueba detecta uno o dos errores, aún puede tener suerte. Más allá de eso, y 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.

Warren Dew
fuente
3
Las pruebas solo pueden revelar la presencia de errores, no su ausencia. La pregunta original se refiere a un problema de 2 hilos, en cuyo caso podrían ser posibles pruebas exhaustivas, pero a menudo no lo es. Para cualquier cosa más allá de los escenarios más simples, es posible que tenga que morder la bala y usar métodos formales, ¡pero no omita las pruebas unitarias! Escribir el código correcto de subprocesos múltiples es difícil en primer lugar, pero un problema igualmente difícil es protegerlo contra la regresión en el futuro.
Paul Williams
44
Increíble resumen de una de las formas menos entendidas. Su respuesta es explotar la segregación real que la gente generalmente pasa por alto.
prash
1
Una docena de segundos es bastante tiempo, incluso si solo tienes unos cientos de pruebas de esa duración ...
Toby Speight
1
@TobySpeight Las pruebas son largas en comparación con las pruebas unitarias normales. Sin embargo, descubrí que media docena de pruebas son más que suficientes si el código de subproceso está diseñado correctamente para ser lo más simple posible: la necesidad de unos cientos de pruebas de subprocesos múltiples seguramente indicaría una disposición de subprocesamiento demasiado compleja.
Warren Dew
2
Ese es un buen argumento para mantener la lógica de su hilo tan separable de la funcionalidad como pueda (lo sé, es mucho más fácil decirlo que hacerlo). Y, si es posible, divida el conjunto de pruebas en conjuntos de "cada cambio" y "precompromiso" (para que sus pruebas minuto a minuto no se vean afectadas demasiado).
Toby Speight
22

También tuve serios problemas al probar código multiproceso. 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 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 (subprocesamiento múltiple, ejecución asincrónica, etc.)

olifante
fuente
20

Hay algunas herramientas que son bastante buenas. Aquí hay un resumen de algunos de los Java.

Algunas buenas herramientas de análisis estático incluyen FindBugs (da 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 tiene que configurar sus propios casos de prueba.

ConTest de IBM Research es interesante. Instrumenta su código insertando todo tipo de comportamientos de modificación de subprocesos (por ejemplo, suspensión 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 como está, pero extremadamente poderoso si sabes cómo usarlo. Muchas herramientas usan SPIN debajo del capó.

MultithreadedTC es probablemente la más convencional, pero definitivamente vale la pena ver algunas de las herramientas de análisis estático mencionadas anteriormente.

xagyg
fuente
16

La capacidad de espera también puede ser útil para ayudarlo a escribir pruebas unitarias deterministas. Le permite esperar hasta que se actualice algún estado en algún lugar 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 para Scala y Groovy.

await until { something() > 4 } // Scala example
Johan
fuente
1
La agilidad es brillante, ¡exactamente lo que estaba buscando!
Forge_7
14

Otra forma de (un poco) probar el código enhebrado, 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:

La prueba de fuzz 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 fallar o al fallar las aserciones de código integradas), 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 a menudo se usan 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 fuzz es una de las técnicas que ofrece una alta relación beneficio / costo.

...

Sin embargo, la prueba fuzz no es 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 comportarse correctamente Por lo tanto, las pruebas de fuzz solo pueden considerarse como una herramienta de búsqueda de errores en lugar de una garantía de calidad.

Robert Gould
fuente
13

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 entrelazados varíen entre iteraciones
  • Cree un throwablecampo y verifíquelo tearDown(vea el Listado 1). Si detecta una mala excepción en otro hilo, simplemente asígnelo a arrojable.
  • Creé la clase utils en el Listado 2 y la encontré invaluable, especialmente waitForVerify y waitForCondition, lo que aumentará en gran medida el rendimiento de sus pruebas.
  • Haz buen uso AtomicBooleanen 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 (por ejemplo, @Test(timeout=60*1000)), ya que las pruebas de concurrencia a veces pueden colgar para siempre cuando se rompen.

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());
}
Kevin Wong
fuente
2
Un tiempo de espera es una buena idea, pero si se agota el tiempo de espera de una prueba, cualquier resultado posterior en esa ejecución es sospechoso. La prueba de tiempo de espera excedido aún puede tener algunos subprocesos en ejecución que pueden desordenarlo.
Don Kirkby
12

Probar la corrección del código MT es, como ya se dijo, un problema bastante 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 (entrelazados) sobre las cuales no tiene mucho control ( sin embargo, asegúrese de leer este artículo). En escenarios simples, podría ser posible probar la corrección 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 / fácil.

Un enfoque que sigo es escribir un código de prueba altamente concurrente para que las carreras de datos potencialmente no detectadas puedan ocurrir. Y luego ejecuté esas pruebas durante un tiempo :) Una vez me topé con una charla en la que un científico de la computación mostraba una herramienta que hace esto (idear aleatoriamente pruebas a partir de especificaciones y luego ejecutarlas salvajemente, al mismo tiempo, buscando los invariantes definidos Estar destrozado).

Por cierto, creo que este aspecto de probar el código MT no se ha mencionado aquí: identifique invariantes del código que puede verificar al azar. Desafortunadamente, encontrar esos invariantes también es un problema bastante difícil. Además, es posible que no se mantengan todo el tiempo durante la ejecución, por lo que debe encontrar / hacer cumplir los puntos de ejecución donde puede esperar que sean ciertos. Llevar la ejecución del código a tal estado también es un problema difícil (y podría en sí mismo incurrir en problemas de concurrencia. ¡Vaya, es muy difícil!

Algunos enlaces interesantes para leer:

  • Intercalación determinista : un marco que permite forzar ciertas entrelazadas de hilos y luego verificar si hay invariantes
  • jMock Blitzer : sincronización de prueba de esfuerzo
  • ClaimConcurrent : versión JUnit de la sincronización de pruebas de estrés
  • Prueba de código concurrente : breve descripción de los dos métodos principales de fuerza bruta (prueba de esfuerzo) o determinista (en busca de los invariantes)
bennidi
fuente
El autor se refiere a la aleatorización en las pruebas. Puede ser QuickCheck , que ha sido portado a muchos idiomas. Puede ver una charla sobre tales pruebas para el sistema concurrente aquí
Max
6

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 subprocesos abstraído de la prueba real. Pete sí menciona que la forma en que lo hago está mal, pero o bien hice la separación correcta o simplemente tuve suerte.

graham.reeds
fuente
66
Leí los dos artículos publicados hasta ahora, y no los encontré muy útiles. Solo habla de las dificultades sin dar muchos consejos concretos. Tal vez los futuros artículos mejoren.
Don Kirkby
6

Para Java, consulte el capítulo 12 de JCIP . Hay algunos ejemplos concretos de escritura de pruebas unitarias deterministas de múltiples subprocesos 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 difícil. Creo que esto es mejor servido por pruebas de integración automatizadas en una variedad de plataformas / configuraciones.

Scott Bale
fuente
6

Me gusta escribir dos o más métodos de prueba para ejecutar en subprocesos paralelos, y cada uno de ellos realiza llamadas al objeto bajo prueba. He estado usando las llamadas Sleep () para coordinar el orden de las llamadas de los diferentes hilos, 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.

Encontré la biblioteca TC Java multiproceso del mismo grupo que escribió FindBugs. Le 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 multiproceso en un pequeño número de clases simples para tener la esperanza de probarlo a fondo.

Una vez que haya probado cuidadosamente los escenarios que espera causar problemas, una prueba no científica que arroje un montón de solicitudes simultáneas a la clase durante un tiempo es una buena manera de buscar problemas inesperados.

Actualización: he jugado un poco con la biblioteca Java de TC multiproceso, y funciona bien. También porté algunas de sus características a una versión .NET que llamo TickingTest .

Don Kirkby
fuente
5

Manejo las pruebas unitarias de componentes roscados de la misma manera que 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 que casi imposible) para aislar por completo.

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

A partir de ahí, puedo inyectar fácilmente el IThreadingManager en mis componentes y usar mi marco de aislamiento de elección para que el hilo se comporte como esperaba durante la prueba.

Eso hasta ahora ha funcionado muy bien para mí, y utilizo el mismo enfoque para el grupo de subprocesos, las cosas en System.Environment, Sleep, etc., etc.

escoria
fuente
5

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.

En resumen, (IMO) no es el uso de un marco sofisticado que garantizará la corrección, sino cómo se diseña el código multiproceso. Dividir las preocupaciones (concurrencia y funcionalidad) es un gran avance 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 ) es 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 raramente garantizan problemas.

¡Buena suerte!

Toby
fuente
También debe mencionar su tempus-fugitbiblioteca aquí, que helps write and test concurrent code;)
Idolon
4

Recientemente descubrí (para Java) una herramienta llamada Threadsafe. Es una herramienta de análisis estático muy similar a findbugs, 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 confiable de Java multihilo.

Incluso detecta algunos problemas potenciales muy sutiles en torno a cosas como la subsunción de clase, el acceso a objetos inseguros a través de clases concurrentes y la detección de modificadores volátiles faltantes cuando se utiliza el paradigma de bloqueo de doble verificación.

Si escribes Java multiproceso, pruébalo .

feldoh
fuente
3

El siguiente artículo sugiere 2 soluciones. Envolviendo un semáforo (CountDownLatch) y agrega funcionalidades como externalizar datos del hilo interno. Otra forma de lograr este propósito es usar Thread Pool (ver Puntos de interés).

Aspersor: objeto de sincronización avanzada

Effi Bar-She'an
fuente
3
Por favor, explique los enfoques aquí, los enlaces externos pueden estar muertos en el futuro.
Uooo
2

Pasé la mayor parte de la semana pasada en una biblioteca universitaria estudiando la depuración de código concurrente. El problema central es que el código concurrente no es determinista. Típicamente, la depuración académica ha caído en uno de los tres campos aquí:

  1. Event-trace / replay. Esto requiere un monitor de eventos y luego revisar los eventos que se enviaron. En un marco UT, esto implicaría enviar manualmente los eventos como parte de una prueba y luego realizar revisiones post mortem.
  2. Scriptable Aquí es donde interactúa con el código en ejecución con un conjunto de disparadores. "En x> foo, baz ()". Esto podría interpretarse en un marco UT donde tiene un sistema de tiempo de ejecución que activa una prueba dada en una determinada condición.
  3. Interactivo. Obviamente, esto no funcionará en una situación de prueba automática. ;)

Ahora, como han notado los comentaristas anteriores, puede diseñar su sistema concurrente en un estado más determinista. Sin embargo, si no lo hace correctamente, acaba de volver a diseñar un sistema secuencial nuevamente.

Mi sugerencia sería centrarse en tener un protocolo de diseño muy estricto sobre lo que se enhebra y lo que no se enhebra. Si restringe su interfaz de modo que haya dependencias mínimas entre los elementos, es mucho más fácil.

Buena suerte y sigue trabajando en el problema.

Paul Nathan
fuente
2

Tuve la desafortunada tarea de probar código enhebrado y definitivamente son las pruebas más difíciles que he escrito.

Al escribir mis pruebas, utilicé una combinación de delegados y eventos. Básicamente se trata de usar PropertyNotifyChangedeventos con un WaitCallbacktipo deConditionalWaiter encuestas.

No estoy seguro de si este fue el mejor enfoque, pero me ha funcionado.

Dale Ragan
fuente
1

Asumir bajo el código "multihilo" significaba algo que es

  • con estado y mutable
  • Y accedido / modificado por múltiples hilos simultáneamente

En otras palabras, estamos hablando de probar una clase / método / unidad segura para subprocesos con estado personalizado , que debería ser una bestia muy rara en la actualidad.

Debido a que esta bestia es rara, antes que nada debemos asegurarnos de que haya todas las excusas válidas para escribirla.

Paso 1. Considere modificar el estado en el mismo contexto de sincronización.

Hoy en día, es fácil escribir código simultáneo y asincrónico componible donde IO u otras operaciones lentas se descargan en segundo plano pero el estado compartido se actualiza y consulta en un contexto de sincronización. p. ej., tareas asíncronas / en espera y Rx en .NET, etc. - todas son comprobables por diseño, las tareas y planificadores "reales" se pueden sustituir para hacer que las pruebas sean deterministas (sin embargo, esto está fuera del alcance de la pregunta).

Puede sonar muy limitado, pero este enfoque funciona sorprendentemente bien. Es posible escribir aplicaciones completas en este estilo sin necesidad de hacer ningún estado seguro para subprocesos (lo hago).

Paso 2. Si la manipulación del estado compartido en el contexto de sincronización única no es absolutamente posible.

Asegúrese de que la rueda no se reinventa / definitivamente no hay una alternativa estándar que pueda adaptarse para el trabajo. Debería ser probable que el código sea muy coherente y esté contenido dentro de una unidad, por ejemplo, con una buena probabilidad de que sea un caso especial de alguna estructura de datos estándar segura para subprocesos, como mapa o colección hash o lo que sea.

Nota: si el código es grande / abarca varias clases Y necesita una manipulación de estado de subprocesos múltiples, entonces hay una gran posibilidad de que el diseño no sea bueno, reconsidere el Paso 1

Paso 3. Si se alcanza este paso, entonces debemos probar nuestra propia clase / método / unidad segura para subprocesos con estado personalizado .

Seré sincero: nunca tuve que escribir pruebas adecuadas para ese código. La mayoría de las veces me salgo en el Paso 1, a veces en el Paso 2. La última vez que tuve que escribir código personalizado seguro para subprocesos fue hace tantos años que fue antes de adoptar las pruebas unitarias / probablemente no tendría que escribirlo con el conocimiento actual de todos modos.

Si realmente tuviera que probar dicho código ( finalmente, la respuesta real ), intentaría un par de cosas a continuación

  1. Prueba de esfuerzo no determinista. Por ejemplo, ejecute 100 hilos simultáneamente y verifique que el resultado final sea consistente. Esto es más típico para pruebas de nivel superior / integración de escenarios de usuarios múltiples, pero también se puede usar a nivel de unidad.

  2. Exponga algunos 'ganchos' de prueba donde la prueba puede inyectar algún código para ayudar a crear escenarios deterministas en los que un hilo debe realizar operaciones antes que el otro. Por feo que sea, no se me ocurre nada mejor.

  3. Pruebas demoradas para hacer que los subprocesos se ejecuten y realicen operaciones en un orden particular. Estrictamente hablando, tales pruebas tampoco son deterministas (existe la posibilidad de que el sistema congele / detenga la colección de GC del mundo que puede distorsionar los retrasos orquestados), también es feo pero permite evitar enganches.

Kola
fuente
0

Para el código J2E, he usado SilkPerformer, LoadRunner y JMeter para la prueba de concurrencia de hilos. Todos hacen lo mismo. Básicamente, le brindan una interfaz relativamente simple para administrar su versión del servidor proxy, requerida, con el fin de analizar el flujo de datos TCP / IP, y simular a múltiples usuarios haciendo solicitudes simultáneas a su servidor de aplicaciones. El servidor proxy puede darle la capacidad 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 envían y modificarlos sistemáticamente para cada usuario. Pero las verdaderas pruebas son cuando se ejecuta en https (Secured Socket Layers). Luego, también tiene que lidiar con la alteración sistemática de la sesión y los datos de 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 basura de Java para cerrar la solicitud de conexión que se estableció al iniciar sesión, al servidor LDAP, al iniciar sesión. Esto dio lugar a que los usuarios estuvieran expuestos a las sesiones de otros usuarios y a resultados muy confusos, al intentar analizar lo que sucedió cuando el servidor se puso 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 desarrollamos el problema descrito anteriormente, es muy útil. Pero estas herramientas son la mejor solución para probar código multiproceso. 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 se trata simplemente de instalar el software y activar la GUI, a menos que ya haya tenido alguna exposición a la programación multiproceso. He tratado de identificar las 3 categorías críticas de áreas para comprender (formularios, sesión y datos de cookies), con la esperanza de que al menos comenzar con la comprensión de estos temas lo ayudará a enfocarse en resultados rápidos, en lugar de tener que leer los toda la documentación

Gallo rojo
fuente
0

La concurrencia es una interacción compleja entre el modelo de memoria, hardware, cachés y nuestro código. En el caso de Java, al menos tales pruebas han sido abordadas en parte 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 son las mircobenchmarks. No grandes aplicaciones comerciales.

Mohan Radhakrishnan
fuente
0

Hay un artículo sobre el tema, que usa Rust como idioma en el código de ejemplo:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

En resumen, el truco es escribir su lógica concurrente para que sea robusta para el no determinismo involucrado con múltiples hilos de ejecución, utilizando herramientas como canales y condvas.

Entonces, si así es como ha estructurado sus "componentes", la forma más fácil de probarlos es mediante el uso de canales para enviarles mensajes, y luego bloquear en otros canales para afirmar que el componente envía ciertos mensajes esperados.

El artículo vinculado está completamente escrito usando pruebas unitarias.

gterzian
fuente
-1

Si está probando un nuevo subproceso simple (ejecutable) .run () Puede burlarse de 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 burlarse de nuevos subprocesos y ejecutar el argumento ejecutable secuencialmente 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 
}
Avraham Shalev
fuente
-3

(si es posible) no use hilos, use actores / objetos activos. Fácil de probar

eneldo
fuente
2
@OMTheEternity quizás, pero sigue siendo la mejor respuesta de la OMI.
Eneldo
-5

Puede usar EasyMock.makeThreadSafe para hacer que la instancia de prueba sea segura

usuario590444
fuente
Esta no es en absoluto una forma posible de probar código multiproceso. El problema no es que el código de prueba se ejecute con subprocesos múltiples, sino que usted pruebe el código que generalmente se ejecuta con subprocesos múltiples. Y no puede sincronizar todo porque ya no prueba las carreras de datos.
bennidi