jueves, 10 de septiembre de 2015

Propuesta de tecnologías java para la realización de tests unitarios y de integración.

Se realizan 2 tipos de tests a nivel general, tests unitarios y tests de integración. Los tests unitarios no levantarán spring ni tendrán ningún tipo de iteracción con sistemas externos y los tests de integración serán los que levanten un servidor embebido y se conecten a un entorno "controlado" tipo Test.

Tests unitarios por método público con JMockit.

Se realizarán tests junit de cada método hasta pasar por todos los caminos lógicos del método. Si nuestro método utiliza otros métodos públicos o de otras clases estos deberán ser mockeados con JMockit. De esta forma forma conseguimos tener tests unitarios totalmente independientes del entorno y de otras clases.
Estos tipos de tests serán desarrollados para:
  • métodos estáticos de clases de converters y de utilidades.
  • métodos de entrada de endpoint
    Y no los generaría para:
  • métodos de acceso a base de datos, cache o derivado.
A nivel de ordenación, en la carpeta de tests deberán existir los mismos paquetes y cada clase testeable deberá tener su clase de tests con el mismo nombre acabado en Test. Ejemplo: MiclaseService.java es la clase testeable y MiclaseServiceTest.java es su clase de tests.
De esta manera es fácil ver qué clases tienen ya tests implementados y cuales son.
Este tipo de tests al no levantar spring son rápidos y se puede tener una gran cantidad sin importar los tiempos.
Estos serán los tests que se ejecutan con nuestro entorno de integración continua para medir la cobertura y de los cuales sacará los informes Sonar.

Tests de integración utilizando el servidor embebido que nos ofrece spring-boot.

Estos tests levantarán un servidor embebido y se conectarán a un entorno "estable/controlado". Estos tests deberían realizar todas las llamadas de endpoints de la aplicación,  las clases que tienen la anotacion @Controller, con todos los posibles parámetros.
 Estos tests nos aseguran que todos los sistemas que se utilizan están alineados al desarrollo, bases de datos, ws de terceros, etc...

Utilidades extra

  • Tests de carga con junit
@RunWith(SpringJUnit4ClassExtendingParamsRunners.class) Utilizamos este Run para poder utilizar parámetros
Para más detalles de uso y posibilidades dejo el siguiente enlace: http://junitparams.googlecode.com

Ejemplo de uso JUnitParams con SpringBoot
Clase generada:
 package utils;  
 import java.lang.reflect.Method;  
 import org.junit.Ignore;  
 import org.junit.Test;  
 import org.junit.internal.runners.model.ReflectiveCallable;  
 import org.junit.internal.runners.statements.ExpectException;  
 import org.junit.internal.runners.statements.Fail;  
 import org.junit.internal.runners.statements.FailOnTimeout;  
 import org.junit.runner.Description;  
 import org.junit.runner.notification.RunNotifier;  
 import org.junit.runners.BlockJUnit4ClassRunner;  
 import org.junit.runners.model.FrameworkMethod;  
 import org.junit.runners.model.InitializationError;  
 import org.junit.runners.model.Statement;  
 import org.springframework.core.annotation.AnnotatedElementUtils;  
 import org.springframework.core.annotation.AnnotationAttributes;  
 import org.springframework.core.annotation.AnnotationUtils;  
 import org.springframework.test.annotation.ProfileValueUtils;  
 import org.springframework.test.annotation.Repeat;  
 import org.springframework.test.annotation.Timed;  
 import org.springframework.test.context.TestContextManager;  
 import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks;  
 import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks;  
 import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks;  
 import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks;  
 import org.springframework.test.context.junit4.statements.SpringFailOnTimeout;  
 import org.springframework.test.context.junit4.statements.SpringRepeat;  
 import org.springframework.util.ReflectionUtils;  
 import junitparams.JUnitParamsRunner;  
 import lombok.extern.slf4j.Slf4j;  
 /**  
  * {@code SpringJUnit4ClassRunner} is a custom extension of JUnit's {@link BlockJUnit4ClassRunner} which provides functionality of the  
  * Spring TestContext Framework to standard JUnit tests by means of the {@link TestContextManager} and associated support classes and  
  * annotations.  
  *  
  *  
  * The following list constitutes all annotations currently supported directly or indirectly by {@code SpringJUnit4ClassRunner}.  
  * (Note that additional  
  * annotations may be supported by various  
  * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}  
  * or {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}  
  * implementations.)  
  *  
  *  
    *  
   {@link Test#expected() @Test(expected=...)}  
    *  
   {@link Test#timeout() @Test(timeout=...)}  
    *  
   {@link Repeat @Repeat}  
    *  
   {@link Ignore @Ignore}  
    *  
   {@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}  
    *  
   {@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}  
    *   
  *  
  *  
  * NOTE: As of Spring Framework 4.1, this class requires JUnit 4.9 or higher.  
  *  
  */  
 @SuppressWarnings("deprecation")  
 @Slf4j  
 public class SpringJUnit4ClassExtendingParamsRunners extends JUnitParamsRunner {  
   private static final Method WITHRULESMETHOD;  
   static {  
     WITHRULESMETHOD =  
       ReflectionUtils.findMethod(SpringJUnit4ClassExtendingParamsRunners.class, "withRules", FrameworkMethod.class, Object.class,  
         Statement.class);  
     if (WITHRULESMETHOD == null) {  
       throw new IllegalStateException("Failed to find withRules() method: SpringJUnit4ClassRunner requires JUnit 4.9 or higher.");  
     }  
     ReflectionUtils.makeAccessible(WITHRULESMETHOD);  
   }  
   private final TestContextManager testContextManager;  
   /**  
    * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a {@link TestContextManager} to provide Spring testing functionality to  
    * standard JUnit tests.  
    *  
    * @param clazz the test class to be run  
    * @see #createTestContextManager(Class)  
    */  
   public SpringJUnit4ClassExtendingParamsRunners(final Class clazz) throws InitializationError {  
     super(clazz);  
     if (log.isDebugEnabled()) {  
       log.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "].");  
     }  
     testContextManager = createTestContextManager(clazz);  
   }  
   /**  
    * Creates a new {@link TestContextManager} for the supplied test class.  
    *  
    * Can be overridden by subclasses.  
    *  
    * @param clazz the test class to be managed  
    */  
   protected TestContextManager createTestContextManager(final Class clazz) {  
     return new TestContextManager(clazz);  
   }  
   /**  
    * Get the {@link TestContextManager} associated with this runner.  
    */  
   protected final TestContextManager getTestContextManager() {  
     return testContextManager;  
   }  
   /**  
    * Returns a description suitable for an ignored test class if the test is  
    * disabled via {@code @IfProfileValue} at the class-level, and  
    * otherwise delegates to the parent implementation.  
    *  
    * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)  
    */  
   @Override  
   public Description getDescription() {  
     if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {  
       return Description.createSuiteDescription(getTestClass().getJavaClass());  
     }  
     return super.getDescription();  
   }  
   /**  
    * Check whether the test is enabled in the first place. This prevents  
    * classes with a non-matching {@code @IfProfileValue} annotation from  
    * running altogether, even skipping the execution of {@code prepareTestInstance()} {@code TestExecutionListener} methods.  
    *  
    * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)  
    * @see org.springframework.test.annotation.IfProfileValue  
    * @see org.springframework.test.context.TestExecutionListener  
    */  
   @Override  
   public void run(final RunNotifier notifier) {  
     if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {  
       notifier.fireTestIgnored(getDescription());  
       return;  
     }  
     super.run(notifier);  
   }  
   /**  
    * Wraps the {@link Statement} returned by the parent implementation with a {@link RunBeforeTestClassCallbacks} statement, thus preserving the  
    * default functionality but adding support for the Spring TestContext  
    * Framework.  
    *  
    * @see RunBeforeTestClassCallbacks  
    */  
   @Override  
   protected Statement withBeforeClasses(final Statement statement) {  
     final Statement junitBeforeClasses = super.withBeforeClasses(statement);  
     return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());  
   }  
   /**  
    * Wraps the {@link Statement} returned by the parent implementation with a {@link RunAfterTestClassCallbacks} statement, thus preserving the  
    * default  
    * functionality but adding support for the Spring TestContext Framework.  
    *  
    * @see RunAfterTestClassCallbacks  
    */  
   @Override  
   protected Statement withAfterClasses(final Statement statement) {  
     final Statement junitAfterClasses = super.withAfterClasses(statement);  
     return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());  
   }  
   /**  
    * Delegates to the parent implementation for creating the test instance and  
    * then allows the {@link #getTestContextManager() TestContextManager} to  
    * prepare the test instance before returning it.  
    *  
    * @see TestContextManager#prepareTestInstance(Object)  
    */  
   @Override  
   protected Object createTest() throws Exception {  
     final Object testInstance = super.createTest();  
     getTestContextManager().prepareTestInstance(testInstance);  
     return testInstance;  
   }  
   /**  
    * Performs the same logic as {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},  
    * except that tests are determined to be ignored by {@link #isTestMethodIgnored(FrameworkMethod)}.  
    */  
   @Override  
   protected void runChild(final FrameworkMethod frameworkMethod, final RunNotifier notifier) {  
     final Description description = describeChild(frameworkMethod);  
     if (isTestMethodIgnored(frameworkMethod)) {  
       notifier.fireTestIgnored(description);  
     } else {  
       runLeaf(methodBlock(frameworkMethod), description, notifier);  
     }  
   }  
   /**  
    * Augments the default JUnit behavior {@link #withPotentialRepeat(FrameworkMethod, Object, Statement) with  
    * potential repeats} of the entire execution chain.  
    *  
    * Furthermore, support for timeouts has been moved down the execution chain in order to include execution of {@link org.junit.Before @Before} and  
    * {@link org.junit.After @After} methods within the timed execution. Note that this differs from the default JUnit behavior of executing  
    * {@code @Before} and {@code @After} methods in the main thread while executing the actual test method in a separate thread. Thus, the end effect  
    * is that {@code @Before} and {@code @After} methods will be executed in the same thread as the test method. As a consequence, JUnit-specified  
    * timeouts will work fine in combination with Spring transactions. Note that JUnit-specific timeouts still differ from Spring-specific timeouts  
    * in that the former execute in a separate thread while the latter simply execute in the main thread (like regular tests).  
    *  
    * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement)  
    * @see #withBefores(FrameworkMethod, Object, Statement)  
    * @see #withAfters(FrameworkMethod, Object, Statement)  
    * @see #withPotentialRepeat(FrameworkMethod, Object, Statement)  
    * @see #withPotentialTimeout(FrameworkMethod, Object, Statement)  
    */  
   @Override  
   protected Statement methodBlock(final FrameworkMethod frameworkMethod) {  
     Object testInstance;  
     try {  
       testInstance = new ReflectiveCallable() {  
         @Override  
         protected Object runReflectiveCall() throws Throwable {  
           return createTest();  
         }  
       }.run();  
     } catch (final Throwable ex) {  
       return new Fail(ex);  
     }  
     Statement statement = methodInvoker(frameworkMethod, testInstance);  
     statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);  
     statement = withBefores(frameworkMethod, testInstance, statement);  
     statement = withAfters(frameworkMethod, testInstance, statement);  
     statement = withRulesReflectively(frameworkMethod, testInstance, statement);  
     statement = withPotentialRepeat(frameworkMethod, testInstance, statement);  
     statement = withPotentialTimeout(frameworkMethod, testInstance, statement);  
     return statement;  
   }  
   /**  
    * Invoke JUnit's private {@code withRules()} method using reflection.  
    */  
   private Statement withRulesReflectively(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement statement) {  
     return (Statement) ReflectionUtils.invokeMethod(WITHRULESMETHOD, this, frameworkMethod, testInstance, statement);  
   }  
   /**  
    * Returns {@code true} if {@link Ignore @Ignore} is present for the supplied {@link FrameworkMethod test method} or if the test method is  
    * disabled via {@code @IfProfileValue}.  
    *  
    * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class)  
    */  
   protected boolean isTestMethodIgnored(final FrameworkMethod frameworkMethod) {  
     final Method method = frameworkMethod.getMethod();  
     return (method.isAnnotationPresent(Ignore.class) || !ProfileValueUtils.isTestEnabledInThisEnvironment(method, getTestClass().getJavaClass()));  
   }  
   /**  
    * Performs the same logic as {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)} except that the  
    * expected exception is retrieved using {@link #getExpectedException(FrameworkMethod)}.  
    */  
   @Override  
   protected Statement possiblyExpectingExceptions(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement next) {  
     final Class expectedException = getExpectedException(frameworkMethod);  
     return expectedException != null ? new ExpectException(next, expectedException) : next;  
   }  
   /**  
    * Get the {@code exception} that the supplied {@link FrameworkMethod  
    * test method} is expected to throw.  
    *  
    * Supports JUnit's {@link Test#expected() @Test(expected=...)} annotation.  
    *  
    * @return the expected exception, or {@code null} if none was specified  
    */  
   protected Class getExpectedException(final FrameworkMethod frameworkMethod) {  
     final Test testAnnotation = frameworkMethod.getAnnotation(Test.class);  
     final Class junitExpectedException =  
       (testAnnotation != null && testAnnotation.expected() != Test.None.class ? testAnnotation.expected() : null);  
     return junitExpectedException;  
   }  
   /**  
    * Supports both Spring's {@link Timed @Timed} and JUnit's {@link Test#timeout() @Test(timeout=...)} annotations, but not both  
    * simultaneously. Returns either a {@link SpringFailOnTimeout}, a {@link FailOnTimeout}, or the unmodified, supplied {@link Statement} as  
    * appropriate.  
    *  
    * @see #getSpringTimeout(FrameworkMethod)  
    * @see #getJUnitTimeout(FrameworkMethod)  
    */  
   @Override  
   protected Statement withPotentialTimeout(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement next) {  
     Statement statement = null;  
     final long springTimeout = getSpringTimeout(frameworkMethod);  
     final long junitTimeout = getJUnitTimeout(frameworkMethod);  
     if (springTimeout > 0 && junitTimeout > 0) {  
       final String msg =  
         "Test method [" + frameworkMethod.getMethod() + "] has been configured with Spring's @Timed(millis=" + springTimeout  
           + ") and JUnit's @Test(timeout=" + junitTimeout  
           + ") annotations. Only one declaration of a 'timeout' is permitted per test method.";  
       log.error(msg);  
       throw new IllegalStateException(msg);  
     } else if (springTimeout > 0) {  
       statement = new SpringFailOnTimeout(next, springTimeout);  
     } else if (junitTimeout > 0) {  
       statement = new FailOnTimeout(next, junitTimeout);  
     } else {  
       statement = next;  
     }  
     return statement;  
   }  
   /**  
    * Retrieves the configured JUnit {@code timeout} from the {@link Test @Test} annotation on the supplied {@link FrameworkMethod test method}.  
    *  
    * @return the timeout, or {@code 0} if none was specified.  
    */  
   protected long getJUnitTimeout(final FrameworkMethod frameworkMethod) {  
     final Test testAnnotation = frameworkMethod.getAnnotation(Test.class);  
     return (testAnnotation != null && testAnnotation.timeout() > 0 ? testAnnotation.timeout() : 0);  
   }  
   /**  
    * Retrieves the configured Spring-specific {@code timeout} from the {@link Timed @Timed} annotation on the supplied {@link FrameworkMethod test  
    * method}.  
    *  
    * @return the timeout, or {@code 0} if none was specified.  
    */  
   protected long getSpringTimeout(final FrameworkMethod frameworkMethod) {  
     final AnnotationAttributes annAttrs = AnnotatedElementUtils.getAnnotationAttributes(frameworkMethod.getMethod(), Timed.class.getName());  
     if (annAttrs == null) {  
       return 0;  
     } else {  
       final long millis = annAttrs.getNumber("millis").longValue();  
       return millis > 0 ? millis : 0;  
     }  
   }  
   /**  
    * Wraps the {@link Statement} returned by the parent implementation with a {@link RunBeforeTestMethodCallbacks} statement, thus preserving the  
    * default functionality but adding support for the Spring TestContext  
    * Framework.  
    *  
    * @see RunBeforeTestMethodCallbacks  
    */  
   @Override  
   protected Statement withBefores(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement statement) {  
     final Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement);  
     return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), getTestContextManager());  
   }  
   /**  
    * Wraps the {@link Statement} returned by the parent implementation with a {@link RunAfterTestMethodCallbacks} statement, thus preserving the  
    * default functionality but adding support for the Spring TestContext  
    * Framework.  
    *  
    * @see RunAfterTestMethodCallbacks  
    */  
   @Override  
   protected Statement withAfters(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement statement) {  
     final Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement);  
     return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), getTestContextManager());  
   }  
   /**  
    * Supports Spring's {@link Repeat @Repeat} annotation by returning a {@link SpringRepeat} statement initialized with the configured repeat  
    * count or {@code 1} if no repeat count is configured.  
    *  
    * @see SpringRepeat  
    */  
   protected Statement withPotentialRepeat(final FrameworkMethod frameworkMethod, final Object testInstance, final Statement next) {  
     final Repeat repeatAnnotation = AnnotationUtils.getAnnotation(frameworkMethod.getMethod(), Repeat.class);  
     final int repeat = (repeatAnnotation != null ? repeatAnnotation.value() : 1);  
     return new SpringRepeat(next, frameworkMethod.getMethod(), repeat);  
   }  
 }  


Forma de uso:

@RunWith(SpringJUnit4ClassAndJUnitParamsRunners.class)

Utilidades extra

  • Categorización de tests
    Junit permite "categorizar" tests utilizando @Category, esto nos permite crear grupos de tests diferenciandolos por lo que queramos, como ejemplo básico he visto que se diferencian por SlowTest y FastTests, en principio no se me ocurren categorias útiles pero quizás si nos puedan ser útiles.
    La categorización nos sirve para realizar lanzamiento de tests personalizados. Imaginad que el total de tests tardará 7 horas, pero realmente solo quisieramos lanzar los tests rápidos que tardan 5 minutos.
    Tutorial de ejemplo:
    http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=JUnitTestRunners
  • Definición de timeouts para tests de integracion
    Junit provee de la opcion de definir timeouts, @Test(timeout=600000), esto deberíamos añadirlo al menos en los tests de integración, ya que si se nos queda algún test bloqueado por base de datos o cualquier otra cosa puede causar problemas a nivel de servidor de integración continua. 
  • Timeout a nivel global: @Rule public Timeout globalTimeout = new Timeout(600000);

No hay comentarios: