Introspección y Reflexión en Java para testear clases de utilidades

0
11834

En este tutorial vamos a ver la potencia de la introspección y reflexión en Java para hacer algo muy poco común: testear atributos de clases de utilidad.

1. Introducción

En los lenguajes dinámicos, como por ejemplo, Javascript, es posible alterar los objetos en tiempo de ejecución, sea cual sea su clase, añadiendo atributos o haciendo comprobaciones sobre ellos. Esto permite hacer algunas operaciones bastante creativas en nuestros desarrollos, o disponer de librerías que hacen «magia».

Pero en los lenguajes estáticos como por ejemplo Java, esto ya es menos común: solemos partir de la base de que tenemos clases prácticamente inamovibles, de las cuales surgen los objetos, y tampoco nos suele cuadrar en la cabeza hacer algún tipo de consulta en tiempo de ejecución sobre sus atributos y propiedades, o directamente su modificación: no cuadra mucho con la programación orientada a objetos puramente dicha.

Como ya sabemos de sobra, Java es un lenguaje fuertemente tipado, pero esto no quiere decir que no permita hacer algunas operaciones especiales, ayudándose de dos conceptos:

  • Introspección: la capacidad para inspeccionar los metadatos de un objeto, como atributos, propiedades, visibilidad…
  • Reflexión: la capacidad para alterar en tiempo de ejecución los metadatos, como añadir atributos, alterar visibilidad…

En este tutorial vamos a usar estas dos capacidades de Java para poder hacer algo un poco «creativo»: testear si una clase de utilidad está correctamente especificada.

1.1 Advertencia.

Os advierto que en este tutorial voy a hacer unas cuantas cosas que seguramente hagan echarse las manos a la cabeza a los más puristas artesanos del software. Las enumero para que veáis que soy consciente de ello:

  • Las clases de utilidades no pueden considerarse Programación Orientada a Objetos pura y dura: son como un cajón de sastre donde van métodos que un mal diseño ha condenado a no pertenecer a ninguna clase.
  • El objetivo es testear si una clase privada es final, si su constructor es privado y si lanza una excepción si se intenta invocar: esto va en contra de la filosofía del agilismo, porque requiere un esfuerzo grande que no merece la pena porque no aporta valor alguno, salvo darse la satisfacción de testear todas las líneas del código, incluida alguna que jamás se podrá ejecutar.
  • La introspección y la reflexión son armas que deberían tratarse con mucho cuidado, y que podría cuestionarse su pertenencia a una programación orientada a objetos como debe de ser.

Así, que… tomaos a este tutorial como un ejercicio creativo de demostración!

2. Clases de Utilidades: a veces no hay más remedio.

Las clases de utilidades son esas clases que aglutinan una serie de métodos que no terminan de encajar en ninguna otra clase. Suelen ser consideradas como un «bad smell», ya que no terminan de encajar con la realidad de la programación orientada a objetos.

Existen numerosas opiniones en su contra, como por ejemplo esta, donde hay muchos enlaces a otras opiniones. Lo cierto es que a veces parecen imprescindibles para salir rápidamente de los problemas, y en mi opinión, son una especie de crédito al banco de la deuda técnica que nos permite continuar usando un lenguaje orientada a objetos.

Sea como fuere, si tienes que usar clases de utilidades, tienes que tener en cuenta que deben cumplir estas condiciones (espero no haber olvidado nada…):

  1. Deben ser clases finales, para que nadie las pueda heredar.
  2. Deben tener un constructor privado, para que no puedan ser instanciadas.
  3. Su constructor debe lanzar un UnsupportedOperationExcepcion, por si alguien, haciendo uso de reflexión, decide hacer público el constructor (mal!!!)
  4. Sus métodos deben ser estáticos, para que puedan ser usados sin necesidad de instanciar la clase.
  5. Y también deben ser estáticos sus atributos.
  6. ¿Algo más? No sé si me ha olvidado algo…

En este tutorial vamos a hacer un test que se encargue de testear estas condiciones… Si nunca has usado la introspección… ¿imaginas cómo lo podrías hacer?

3. Sonarqube y la manía de cuantificar todo

Como ya he indicado en el apartado de advertencia, testear que una clase de utilidades tiene esas características, no aporta casi nada, y si lo aporta, es a una relación de coste muy elevada para el rendimiento obtenido…

Pero a los clientes, que no suelen ser tan expertos como nosotros en el mundo del desarrollo, para que nos permitan hacer TDD o al menos algunos test y no rechisten mucho a la hora de asumir su coste, les hemos tenido que «vender la moto» de lo bueno que es el testing. Y como la gente que maneja pasta de los presupuestos no es tonta, nos habrán pedido alguna evidencia objetiva de lo bueno que es tener test y cuántos tests hay en un proyecto y cuantos no hay en otro. Aquí entra en juego SonarQube.

SonarQube, entre otros aspectos, usa la cobertura de test unitarios para dar una puntuación de la deuda técnica, o lo que el cliente entiende: lo bueno o malo que es su producto. Y si enseñamos una clase estática, SonarQube provocará una conversación de este tipo:

  • Cliente: en esta clase tienes un 0% de cobertura de test.
  • Programador: ya, es que es una clase estática que sólo tiene constantes, no hace falta probarla.
  • Cliente: sí, lo que tú quieras, pero la cobertura de test tú módulo está al 80% mientras que la de otro está al 95%… no está por encima del 90% como hemos firmado
  • Programador: vale, pero entiende que este caso…
  • Cliente: no, no, hemos firmado un acuerdo por el que todos los tests tiene que estar por encima del 90% de cobertura… no es mi problema.
  • Programador: mmm pero es que esto no es agilismo…
  • Cliente: yo tampoco seré ágil a la hora de ordenar el pago de tu trabajo de consultoría…

Bueno, tranquilo 🙂 es sólo una dramatización para justificar la barbaridad en contra del agilismo que voy a perpetrar a continuación, y no es otra que satisfacer al cliente !A TODA COSTA!

4. La implementación

El objetivo que me he marcado ha sido crear una clase abstracta que pueda ser extendida por otras clases de test, y que incluya los test de las condiciones que hemos enumerado en el apartado 2 de clase de utilidad.

Si haces las cosas bien, por cada clase que tengas en tu código, deberías tener otra clase que incluya sus test unitarios (si consideras que tu «unidad» es una clase – así debería ser si respetas un poco SOLID). Así, cuando tengas la clase de utilidad «UtilClass.java», deberías tener en tu apartado de test una clase llamada «UtilClassTest.java». O al menos eso dice la teoría.

El resultado lo puedes consultar en un GitHub que he creado para este proyecto: https://github.com/4lberto/testUtilityClasses. Podrás encontrar tanto la clase como ejemplos de test con todas las casuísticas.

La clase abstracta que nos permite testear las condiciones de una clase de utilidades es la siguiente:

public abstract class AbstractUtilityBaseTester<T> {

	@SuppressWarnings("unchecked")
	@Test
	public void shouldTestPrivateConstructor() {
		Constructor<T> constructor;
		Class<T> persistentClass = null;
		try {
			persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
					.getActualTypeArguments()[0];

			checkFinalClass(persistentClass);

			constructor = checkConstructor(persistentClass);

			checkMethodsAreStatic(persistentClass);
			checkFieldsAreStatic(persistentClass);
			checkInstanciation(constructor, persistentClass);
		} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException
				| InvocationTargetException e) {
			Assert.assertEquals(
					"The cause of the inner exception must be UnsupportedOperationException when trying to instanciate the class "
							+ persistentClass.getName() + e.getClass().getName(),
					UnsupportedOperationException.class.getName(), e.getCause().getClass().getName());
		}
	}

	private void checkFinalClass(final Class<T> persistentClass) {
		Assert.assertTrue("The class " + persistentClass.getName() + " must be final",
				Modifier.isFinal(persistentClass.getModifiers()));
	}

	private Constructor<T> checkConstructor(final Class<T> persistentClass) {
		Constructor<T> constructor;
		constructor = (Constructor<T>) persistentClass.getDeclaredConstructors()[0];
		Assert.assertTrue("The constructor of the class " + persistentClass.getName() + ", must be private",
				Modifier.isPrivate(constructor.getModifiers()));
		constructor.setAccessible(true);
		return constructor;
	}

	private void checkInstanciation(final Constructor<T> constructor, final Class<T> persistentClass)
			throws InstantiationException, IllegalAccessException, InvocationTargetException {
		final T instance = constructor.newInstance();
		Assert.assertTrue("No excepction is thrown when instanciating " + persistentClass.getName()
				+ " a UnsupportedOperationException should be thrown", false);
	}

	private void checkMethodsAreStatic(final Class<T> persistentClass) {
		final Method[] methods = persistentClass.getDeclaredMethods();
		for (int i = 0; i < methods.length; i++) {
			Assert.assertTrue(
					"Method " + methods[i].getName() + " of the class " + persistentClass.getName()
							+ " is NOT STATIC. All methods must be static",
					Modifier.isStatic(methods[i].getModifiers()));
		}
	}

	private void checkFieldsAreStatic(final Class<T> persistentClass) {
		final Field[] fields = persistentClass.getDeclaredFields();
		for (int i = 0; i < fields.length; i++) {
			Assert.assertTrue(
					"Field " + fields[i].getName() + " of the class " + persistentClass.getName()
							+ " is NOT STATIC. All methods must be static",
					Modifier.isStatic(fields[i].getModifiers()));
		}
	}

}

Su uso no puede ser más sencillo: si tenemos que testear la clase TypicalCorrectUtilityClass.java con el siguiente contenido:

public final class TypicalCorrectUtilityClass {

  	public static final float PI = 3.14f;

  	private TypicalCorrectUtilityClass() {
  		throw new UnsupportedOperationException("Not instanciable class!");
  	}

  	public static int convertToNumber(final String numberString) {
  		int number;
  		switch (numberString) {
  		case "uno":
  			number = 1;
  			break;
  		case "dos":
  			number = 2;
  			break;
  		default:
  			number = -1;
  			break;
  		}
  		return number;
  	}
}

Simplemente deberemos crear una clase de test haciendo uso de los genéricos para indicar la clase de utilidad:

public class TypicalCorrectUtilityClassTest
    extends AbstractUtilityBaseTester<TypicalCorrectUtilityClass> {
    ...
}

¡Qué fácil! Luego dentro de la clase ya se pueden lanzar otros test que comprueben el contenido, pero ya de por sí, al haber heredado de la clase AbstractUtilityBaseTester indicando la clase que se va a testear, se lanza el testeo de todos los puntos que debe cumplir una clase de utilidades

5. Los detalles

Vamos a ver los detalles del código que resultan de interés de la clase AbstractUtilityBaseTester:

5.1. Usando genéricos

Como lo que vamos a comprobar no es específico de ninguna clase en particular, hemos decidido utilizar genéricos para parametrizar la clase a probar. Así, mediante T le indicamos que cuando se realiza una extensión o implementación, se expresará sobre qué clase se va a proceder.

public abstract class AbstractUtilityBaseTester<T> {

El resto de código está condicionado por este genérico T. Cuando veamos T en realidad debemos ver «una clase».

5.2. Adivinando la clase en tiempo de ejecución.

Como usamos genéricos, para sacar la clase debemos hacer una operación un poco complicada y especial. Es el precio que hay que pagar en Java por haber incorporado los genéricos. La verdad es que no es nada sencillo de entender, pero puedes ir mirando cosas como el type erasure para que entiendas la «ñapa» que tuvieron que hacer a la hora de compilar los genéricos, y por qué se debe determinar en tiempo de ejecución las clases.

Entre tú y yo… esto lo he sacado de stackoverflow.

Class<T> persistentClass = null;
  try {
  	persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
  	.getActualTypeArguments()[0];

Una vez que ya sabemos qué clase tenemos que chequear, que será la indicada en T, ¡podemos ponernos manos a la masa con la introspección y la reflexión!

5.3. ¿Es la clase final?

Muy sencillo. Además lo he separado en método privado, como a Uncle Bob le gusta 🙂

private void checkFinalClass(final Class<T> persistentClass) {
	Assert.assertTrue("The class " + persistentClass.getName() + " must be final",
			Modifier.isFinal(persistentClass.getModifiers()));
}

Simplemente usamos el paquete de reflection de java, concretamente a java.lang.reflect.Modifier y su método isFinal, que nos va a decir si el resultado del método getModifiers de la clase es final o no. Lo introducimos en un Assert de JUnit con su correspondiente mensaje explicativo.

5.4. ¿Es el constructor privado?

Ahora simplemente tenemos que preguntarle a la clase por su constructor. Esperemos que sólo tenga uno ^_^ (también lo podríamos haber metido en un check: hazme un pull request y lo incluyo :P)

private Constructor<T> checkConstructor(final Class<T> persistentClass) {
		Constructor<T> constructor;
		constructor = (Constructor<T>) persistentClass.getDeclaredConstructors()[0];
		Assert.assertTrue("The constructor of the class " + persistentClass.getName() + ", must be private",
				Modifier.isPrivate(constructor.getModifiers()));
		constructor.setAccessible(true);
		return constructor;
	}

El constructor también parametrizado con la clase T. Se sacan los constructores declarados con getDeclaredConstructors(). ¡Ojo!, que si usamos la función getConstructors nos devuelve también los constructores de las clases padres… Con declared, tanto para constructores como para otros elementos, indica que queremos los de la propia clase, nada de los heredados.

De igual modo, con Modifier.isPrivate comprobamos si los modifiers del constructor indica si es privado o no, y se aplica el Assert correspondiente.

Ahora viene una cosa interesante… tomamos el constructor y lo convertimos en ¡público! Con esto vamos a ser capaces de acceder a su interior en el test unitario, cargándonos por el camino la encapsulación… ¿Ves como es posible saltarse los «private» que pensabas que protegían tu código?. Veremos más delante para qué lo queremos…

5.5. ¿Son los métodos y atributos estáticos?

Al ser una clase no instanciable, todos los métodos y atributos no pueden residir en un objeto sino en la propia clase, por lo que deben ser estáticos. Se comprueba de forma muy similar los métodos y los atributos: con nuestro amigo Modifier.is…:

private void checkMethodsAreStatic(final Class<T> persistentClass) {
  		final Method[] methods = persistentClass.getDeclaredMethods();
  		for (int i = 0; i < methods.length; i++) {
  			Assert.assertTrue(
  					"Method " + methods[i].getName() + " of the class " + persistentClass.getName()
  							+ " is NOT STATIC. All methods must be static",
  					Modifier.isStatic(methods[i].getModifiers()));
  		}
  	}

  	private void checkFieldsAreStatic(final Class<T> persistentClass) {
  		final Field[] fields = persistentClass.getDeclaredFields();
  		for (int i = 0; i < fields.length; i++) {
  			Assert.assertTrue(
  					"Field " + fields[i].getName() + " of the class " + persistentClass.getName()
  							+ " is NOT STATIC. All methods must be static",
  					Modifier.isStatic(fields[i].getModifiers()));
  		}
  	}

Simplemente se recogen los métodos y atributos declarados en un vector y se recorren uno por uno, aplicando Modifiers.isStatic sobre cada elemento. En el momento que uno no lo sea, nos saltará el assert con el mensaje que permite encontrar exactamente el atributo o método que no cumple la condicion. Recuerda que los test deben informar exactamente de dónde está el error para que pueda ser subsanado de la forma más rápida posible.

5.6. ¿Lanza una excepción al ser instanciado?

Esto ya es una sobreprotección frente al private del constructor, y también un convenio que se suele utilizar por si alguien, como hemos visto, cambia en tiempo de ejecución la visibilidad del constructor: se debe devolver un UnsupportedOperationException.

Debemos recordar en este punto, que cuando se ha comprobado si el constructor era privado, después de la comprobación, se ha cambiado la visibilidad a public mediante:

constructor.setAccessible(true);

Entonces, ya podemos hacer una instancia de nuestra clase y hacer que la ejecución pase por el interior del constructor. Si está vacío o tiene algún código, se ejecutará, y eso es algo que no se quiere. Para evitarlo se lanzará una excepción con el aviso (¡esto no se puede cambiar por reflexión! – al menos que yo sepa jeje).

private void checkInstanciation(final Constructor<T> constructor, final Class<T> persistentClass)
    throws InstantiationException, IllegalAccessException, InvocationTargetException {
  final T instance = constructor.newInstance();
  Assert.assertTrue("No excepction is thrown when instanciating " + persistentClass.getName()
      + " a UnsupportedOperationException should be thrown", false);
}

Simplemente llamamos al newInstance del constructor modificado a public y esperamos que salte o no una excepción. Si no salta es que ha podido hacerlo y que dentro del constructor no se está lanzando nada, por lo que lanzamos un assert con fallo y un mensaje descriptivo.

En el caso de que se lance una excepcion, debemos comprobar que es la excepción que se lanza por convenio: UnsupportedOperationException.

Assert.assertEquals(
					"The cause of the inner exception must be UnsupportedOperationException when trying to instanciate the class "
							+ persistentClass.getName() + e.getClass().getName(),
					UnsupportedOperationException.class.getName(), e.getCause().getClass().getName());

Como la excepción irá dentro de una excepción que actúa de wrapper, se toma la causa y se comprueba de qué clase se trata para hacer el assert.

Y poco más que comentar… es solamente una clase de pocas líneas. Puedes bajarte un proyecto de maven para java +1.7 desde el GitHub que he hecho para el proyecto:https://github.com/4lberto/testUtilityClasses. ¡Úsala como te de la gana!

6. Conclusiones

Hemos visto cómo usar la introspección y reflexión en Java para hacer un poco de «magia» y testear los atributos y modificadores de clases de utilidades, aumentando la cobertura de código hasta niveles poco recomendables en el mundo del agilismo, pero sí compatibles con el ego de un perfeccionista :). Te recomiendo que profundices un poco en el tema de la reflexión en Java porque a veces puede ser de gran utilidad: Java Trail: reflection.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad