viernes, 16 de septiembre de 2022

Testing de procesos concurrentes

En muchas ocasiones nos encontramos con la necesidad de probar hasta que punto un proceso es "thread safe", en general este tipo de pruebas son muy difíciles y en ciertas situaciones nunca lograremos descartar completamente problemas de concurrencia mediante testing. En cualquier caso voy a hablar de una de tantas opciones, en la cual haremos uso de las características propias de la librería de testing TestNG (https://es.wikipedia.org/wiki/TestNG), una pequeña búsqueda arroja multitud de resultados enfrentando TestNG contra JUnit (ej: https://www.baeldung.com/junit-vs-testng) por lo que no voy a entrar en ese tema, me voy a limitar a exponer un ejemplo muy sencillo de uso, puedes encontrar el código de ejemplo en https://github.com/andrestraspuesto/testing-concurrency-testng.

 

Escenario a probar

El ejemplo sobre el que voy a hacer la demo consiste en un contador que será incrementado de forma concurrente. He creado 2 implementaciones: una que controla el acceso concurrente y otra que no.

Interfaz del contador:
public interface Counter {
int increaseAndGet();
int peekCounter();
}


Implementación insegura:
public class NaiveCounter implements Counter {
private int counter;

@Override
public int increaseAndGet() {
return ++counter;
}

@Override
public int peekCounter() {
return counter;
}
}

Implementación segura:
public class ThreadSafeCounter implements Counter {
private AtomicInteger counter = new AtomicInteger(0);

@Override
public int increaseAndGet() {
return counter.incrementAndGet();
}

@Override
public int peekCounter() {
return counter.get();
}
}

Implementación del handler del contador:
public class CounterHandler implements Callable<Integer>{

private final Counter counter;
public CounterHandler(Counter counter) {
this.counter = counter;
}
@Override
public Integer call() {
return counter.increaseAndGet();
}

}

Si se revisa la implementación insegura (NaiveCounter) es fácil deducir que ante un acceso concurrente es probable que el contador tome valores incorrectos.

Tests

A continuación se muestran las clases de test, por motivos didácticos se ha duplicado la clase, aunque perfectamente se podría haber utilizado una sola clase utilizando una factoría para insertar los diferentes contadores a probar.
Test de la clase NaiveCounter:
public class NaiveCounterTest
{
private final NaiveCounter naiveCounter = new NaiveCounter();

@Test(threadPoolSize = 4, invocationCount = 2000)
public void shouldIncreaseCounter() {
Thread.yield();
CounterHandler handler = new CounterHandler(naiveCounter);
Integer result = handler.call();
Assert.assertTrue(result > 0);
}

@Test(dependsOnMethods = {"shouldIncreaseCounter"})
public void shouldBe100() {
Assert.assertEquals(naiveCounter.peekCounter(), 2000);
}
}

Test de la clase ThreadSafeCounter:
public class ThreadSafeCounterTest {

private final ThreadSafeCounter concurrentCounter =
                                    new ThreadSafeCounter();

@Test(threadPoolSize = 4, invocationCount = 2000)
public void shouldIncreaseCounter() {
Thread.yield();

CounterHandler handler = new CounterHandler(concurrentCounter);
Integer result = handler.call();
Assert.assertTrue(result > 0);
}

@Test(dependsOnMethods = {"shouldIncreaseCounter"})
public void shouldBe2000() {
Assert.assertEquals(concurrentCounter.peekCounter(), 2000);
}
}

La estructura de la clase de test tiene 2 métodos de test:

Método shouldIncreaseCounter: la finalidad de este método es ejecutar concurrentemente el handler sobre el contador, para esta finalidad se usan los argumentos de la anotación test:
  • threadPoolSize: determina el tamaño del pool de hilos de ejecución , esto quiere decir que establece el número de ejecuciones concurrentes de este método.
  • invocationCount: establece cuantas veces se debe ejecutar este método
En este caso, hemos decidido que este método debe ejecutarse 2000 veces con 4 hilos concurrentes, de esta forma como pasamos el contador a cada instancia del handler, si no ha habido problemas de concurrencia el contador debería tomar valor 2000.

Método shouldBe2000: este método lo utilizamos para verificar que el contador refleja las 2000 ejecuciones. Para esto se ha utilizado el atributo dependsOnMethods que evita que el test se ejecute antes de que finalice la ejecución de shouldIncreaseCounter.

En la siguiente imagen se muestra el resultado de ejecutar los test (mvn test), donde se ve que falla la ejecución de NaiveCounterTest, porque no coincide el valor del contador.



Por último remarcar el hecho de que este tipo de test facilita las pruebas concurrentes pero no son una garantía absoluta y dependiendo de la situación podrían darse falsos positivos (entendiendo como tal que pase el test aun existiendo fallos de diseño que puedan derivar en fallos de concurrencia).








viernes, 18 de septiembre de 2015

Implementación de un monticulo o heap en javascript.

El montículo es una estructura que siempre tiene como primer elemento el mayor, o el mínimo si es un montículo de mínimos, con un coste muy bajo. Para más información os animo a empezar por wikipedia.

Características de la implementación:
  1. Al construir el montón se debe pasar un boolean con valor true si se quiere que sea de máximos, sino será de mínimos.
  2. Los objetos se insertarán en el heap dentro de un envoltorio (collections.Heap.element) junto con una función que que acepte un parámetro y devuelva true si el objeto "envuelto" es menor que el parámetro.
  3. La estructura binaryHeap forma parte del módulo collections al que se irán añadiendo otras.
En este enlace se puede descargar un rar que contiene la implementación de la estructura y unos test con QUnit.

Lo siguiente es un ejemplo de su utilización:


    var q = new collections.Heap(true);

    function isSmallerFunction(other){
      return this.value < other.value;
    };
    q.insert(new q.element(5, isSmallerFunction));
    q.insert(new q.element(3, isSmallerFunction));
    q.insert(new q.element(7, isSmallerFunction));
    q.insert(new q.element(1, isSmallerFunction));
    q.insert(new q.element(8, isSmallerFunction));
    q.insert(new q.element(8, isSmallerFunction));
    q.insert(new q.element(9, isSmallerFunction));
    q.insert(new q.element(0, isSmallerFunction));

    console.log(q.toString());
    
    while(q.size > 0 ){
      var aux = q.pop();
      console.log(aux.value);
    }

jueves, 26 de junio de 2014

Integración de test QUnit con JUnit 4

En esta entrada (http://misiniciosenjava.blogspot.com.es/p/blog-page_26.html) tienes una prueba de concepto de la integración del resultado test de QUnit con el reporte de test de JUnit de una forma muy muy sencilla.
Integración de los resultados de test de QUnit en el reporte de JUnit.

viernes, 14 de junio de 2013

domingo, 24 de febrero de 2013

Algoritmos divide y vencerás.

¿Conoces los algoritmos del tipo divide y vencerás? En la sección "Algo de algoritmos" tienes una entrada con ejemplos de este tipo de algoritmos.