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


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 se ha ido la gente probando el código que se basa en los hilos para una ejecución exitosa, o simplemente cómo la gente ha ido probando ese tipo de problemas que solo aparecen cuando dos hilos interactúan de una manera determinada.

Esto parece ser un problema realmente importante para los programadores de hoy, sería útil poner en común nuestro conocimiento sobre este tema.


584
2017-08-15 11:44


origen


Respuestas:


Mira, no hay una manera fácil de hacer esto. Estoy trabajando en un proyecto que es intrínsecamente multiproceso. Los eventos vienen del sistema operativo y tengo que procesarlos al mismo tiempo.

La forma más simple de tratar con la prueba de código de aplicación compleja y multihilo es ésta: si es demasiado complejo para probarlo, lo está haciendo mal. Si tiene una única instancia que tiene múltiples hilos que actúan sobre ella, y no puede probar las situaciones en las que estos hilos se superponen uno con otro, entonces su diseño debe ser rehecho. Es tan simple y tan complejo como esto.

Hay muchas maneras de programar para multihilo que evita que los hilos se ejecuten a través de instancias al mismo tiempo. Lo más simple es hacer que todos tus objetos sean inmutables. Por supuesto, eso no suele ser posible. Por lo tanto, debe identificar los 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, aislará algunas clases donde realmente ocurre el multihilo, reduciendo la complejidad general de probar su sistema.

Pero debes darte cuenta de que incluso haciendo esto todavía no puedes probar cada situación donde dos hilos se pisan. Para hacer eso, tendrías que ejecutar dos hilos al mismo tiempo en la misma prueba, y 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 codifiques 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 enhebrar problemas es a través del análisis estático del código. Si su código enhebrado no sigue un conjunto finito de patrones de seguridad de subprocesos, entonces es posible que tenga un problema. Creo que Code Analysis en VS contiene algunos conocimientos de enhebrado, pero probablemente no mucho.

Mira, como están las cosas actualmente (y probablemente sea un buen momento para venir), la mejor manera de probar aplicaciones multiproceso es reducir la complejidad del código enhebrado tanto como sea posible. Minimice las áreas donde los hilos interactúan, pruebe de la mejor manera posible y use el análisis de códigos para identificar áreas peligrosas.


215
2017-08-15 13:29



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

kleolb02La respuesta es buena. Trataré de entrar en más detalles.

Hay una manera, que practico para el código C #. Para las pruebas unitarias, debe ser capaz de programar reproducible pruebas, que es el mayor desafío en el código multiproceso. Así que mi respuesta apunta a forzar el código asíncrono en un arnés de prueba, que funciona sincrónicamente.

Es una idea del libro de Gerard Meszardos "xUnit Patterns de prueba"y se llama" Objeto humilde "(p. 695): debe separar el código lógico central y todo lo que huele 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 de la lógica del núcleo en un sincrónico camino. Usted tiene control absoluto sobre el tiempo de las llamadas que está haciendo en la lógica del núcleo y por lo tanto puede hacer reproducible pruebas. Y esta es su ganancia al separar la lógica del núcleo y la lógica asíncrona.

Esta lógica central necesita ser envuelta por otra clase, que es responsable de recibir llamadas a la lógica central de forma asíncrona y delegados estas llamadas a la lógica central. El código de producción solo tendrá acceso a la lógica del núcleo 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 unitarias para esta clase trabajadora asíncrona como mínimo.

Cualquier cosa por encima de eso (probando la interacción entre clases) son pruebas de componentes. También en este caso, debe poder tener control absoluto sobre el tiempo, si se apega al patrón "Objeto humilde".


79
2018-01-18 12:30



¡Difícil de hecho! En mis pruebas unitarias (C ++), he desglosado esto en varias categorías según las líneas del patrón de simultaneidad utilizado:

  1. Pruebas unitarias para clases que operan en un solo subproceso y que no tienen ningún tipo de subprocesos. Fácil, pruebe como de costumbre.

  2. Pruebas unitarias para Monitorear objetos (aquellos que ejecutan métodos sincronizados en el hilo de control de las personas que llaman) que exponen una API pública sincronizada: crear instancias de varios hilos falsos que ejercen la API. Construya escenarios que ejerzan las condiciones internas del objeto pasivo. Incluya una prueba de ejecución más larga que, básicamente, supera a los demás por 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 ser de bloqueo o de no bloqueo, las personas que llaman pueden obtener futuros, los datos pueden llegar a las colas o deben ser eliminados de la cola. Aquí hay muchas combinaciones posibles; caja blanca de distancia. Todavía requiere múltiples hilos falsos para hacer llamadas al objeto bajo prueba.

Como un aparte:

En la capacitación interna de desarrolladores que hago, enseño Pilares de la concurrencia y estos dos patrones como el marco primario para pensar y descomponer los problemas de concurrencia. Obviamente hay conceptos más avanzados, pero he descubierto que este conjunto de conceptos básicos ayuda a los ingenieros a no salir de la sopa. También conduce a un código que es más verificable por unidad, como se describió anteriormente.


55
2017-08-15 13:26



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 a la pregunta sobre las pruebas. Mi respuesta está dirigida a los casos donde no hay alternativa al código multiproceso; Sí cubro los problemas de diseño de código para que estén completos, pero también discuto 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 esta forma, el procesamiento de datos se puede probar como un código de subproceso único, y lo único que hace el código multiproceso es coordinar los subprocesos.

Lo segundo a 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 colapsarán en la producción, serán difíciles de reproducir incluso en 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 los errores fáciles son corregidos y los errores peligrosos aún están allí.

En cambio, cuando se escribe código multiproceso, primero debe escribir el código con la actitud de que va a evitar escribir los errores. Si ha eliminado correctamente el código de procesamiento de datos, el código de manejo de hilos debería ser lo suficientemente pequeño, preferiblemente unas pocas líneas, en el peor de unas pocas docenas de líneas, que tiene la oportunidad de escribirlo sin escribir un error, y sin escribir muchos errores. , si comprende enhebrar, tómese su tiempo y sea cuidadoso.

Pruebas de unidad de escritura para código multiproceso

Una vez que el código multiproceso se escribe tan cuidadosamente como sea 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 las condiciones de carrera - sino para probar que su estrategia de bloqueo para prevenir dichos errores permite que varios hilos interactúen como se pretendía. .

Para probar correctamente el comportamiento de bloqueo correcto, una prueba debe iniciar varios hilos. Para hacer que la prueba sea repetible, queremos que las interacciones entre los hilos ocurran en un orden predecible. No queremos sincronizar externamente los hilos en la prueba, porque eso enmascara errores que podrían ocurrir en la producción donde los hilos 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 utilizado con éxito cada vez que he tenido que escribir pruebas de código multiproceso.

Si los retrasos son demasiado cortos, la prueba se vuelve frágil, ya que las diferencias de tiempo menores, por ejemplo, entre las diferentes máquinas en las que se pueden ejecutar las pruebas, pueden provocar que el cronometraje se desconecte y la prueba falle. Lo que típicamente he hecho es comenzar con retrasos que causan fallas en las pruebas, aumentar los retrasos para que la prueba pase confiablemente en mi máquina de desarrollo, y luego duplicar los retrasos más allá de eso para que la prueba tenga buenas posibilidades de pasar a otras máquinas. Esto significa que la prueba tomará un tiempo macroscópico, aunque, según mi experiencia, un diseño de prueba cuidadoso puede limitar ese tiempo a no más de una docena de segundos. Dado que no debe haber muchos lugares que requieran el código de coordinación de subprocesos en su aplicación, eso debería ser aceptable para su suite de pruebas.

Finalmente, haga un seguimiento de la cantidad de errores detectados por su prueba. Si su prueba tiene un 80% de cobertura de código, se puede esperar que capture aproximadamente el 80% de sus errores. Si su prueba está bien diseñada pero no encuentra errores, existe la 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 todavía 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.


32
2017-09-11 21:00



También tuve serios problemas para probar el código de subprocesos múltiples. Entonces encontré una solución 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 (multihilo, ejecución asincrónica, etc.)


20
2017-08-20 12:29



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 (da algunos consejos útiles), JLint, Pathfinder de Java (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.

Concurso de IBM Research es interesante. Instrumenta su código insertando todo tipo de comportamientos modificadores de hilos (por ejemplo, dormir y rendimiento) para tratar de descubrir errores aleatoriamente.

GIRAR es una herramienta realmente genial para modelar sus componentes Java (y otros), pero necesita tener un marco útil. Es difícil de usar tal cual, pero extremadamente poderoso si sabes cómo usarlo. Algunas herramientas usan SPIN debajo del capó.

MultithreadedTC es probablemente la más común, pero algunas de las herramientas de análisis estático enumeradas anteriormente definitivamente valen la pena mirar.


16
2017-07-08 12:23



He hecho mucho de esto, y sí, apesta.

Algunos consejos:

  • GroboUtils para ejecutar múltiples hilos de prueba
  • alphaWorks ConTest para instrumentar clases para causar intercalaciones que varían entre iteraciones
  • Crear un throwable campo y verifíquelo tearDown (ver el Listado 1). Si detecta una mala excepción en otro hilo, simplemente asígnelo a throwable.
  • Creé la clase utils en el Listado 2 y la he encontrado invaluable, especialmente waitForVerify y waitForCondition, lo que aumentará en gran medida el rendimiento de tus pruebas.
  • Hacer 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 cosas por el estilo. 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 están rotas

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

11
2017-09-24 04:58



Expectativa también puede ser útil para ayudarte a escribir pruebas determinísticas de unidades. 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 Scala y Groovy.

await until { something() > 4 } // Scala example

11
2017-11-19 17:26