java - Dilema de pruebas unitarias: usar un origen de datos JNDI sin ejecutar JBoss o Spring
eclipse unit-testing (4)
Planteamiento del problema
Quiero poder ejecutar pruebas de junit en métodos que se conectan a una base de datos.
Configuración actual
Eclipse Java EE IDE : el código Java no utiliza ningún marco. Los desarrolladores (incluido yo) quieren pruebas más sólidas del código heredado actual ANTES de intentar mover el código a un marco Spring para que podamos demostrar en el camino que el comportamiento sigue siendo correcto.
JBoss 4.2 - Limitación de versión por software del proveedor ( Adobe LiveCycle ES2 ); Nuestra aplicación web Java utiliza esta configuración de JBoss para ejecutar y utiliza la API de Adobe LiveCycle.
No hemos podido ejecutar con éxito el proveedor configurado de JBoss dentro de Eclipse ; llevamos semanas intentando esto, incluido el contacto con la compañía que brinda nuestro soporte para la configuración de JBoss para Adobe LiveCycle. Supuestamente, el problema es un problema de limitación de memoria con la configuración en Eclipse, pero el cambio de la configuración de la memoria hasta ahora ha fallado en un inicio exitoso del servidor JBoss dentro de Eclipse. Por ahora, el intento de que JBoss se ejecute dentro de Eclipse está en espera.
La conexión de la base de datos se define en una fuente de datos JNDI que JBoss carga al iniciarse. Tanto nuestra aplicación web como Adobe LiveCycle necesitan crear conexiones a esta fuente de datos.
Código
En este fragmento de código, estoy pasando por alto la verificación de errores y la estructura de clase para centrarme en el corazón del asunto. Esperemos que eso no cause problemas a los demás. El texto entre corchetes no es texto real.
Nuestro código para crear la conexión es algo como esto:
Properties props = new Properties();
FileInputStream in = null;
in = new FileInputStream(System.getProperty("[Properties File Alias]"));
props.load(in);
String dsName = props.getProperty(“[JNDI data source name here]”);
InitialContext jndiCntx = new InitialContext();
DataSource ds = (DataSource) jndiCntx.lookup(dsName);
Ds.getConnection();
Quiero poder probar los métodos que dependen de este código sin realizar ningún cambio.
Referencia al alias del archivo de propiedades en el archivo properties-service.xml:
<!-- ==================================================================== -->
<!-- System Properties Service -->
<!-- ==================================================================== -->
<!-- Allows rich access to system properties.-->
<mbean code="org.jboss.varia.property.SystemPropertiesService"
name="jboss:type=Service,name=SystemProperties">
<attribute name="Properties">
[Folder Alias]=[filepath1]
[Properties File Alias]=[filepath2]
</attribute>
</mbean>
Fragmento del archivo de propiedades ubicado en filepath2
[JNDI data source name]=java:/[JNDI data source name]
El archivo xml JNDI para esta fuente de datos se configura así:
<datasources>
<local-tx-datasource>
<jndi-name>[JNDI data source name here]</jndi-name>
<connection-url>jdbc:teradata://[url]/database=[database name]</connection-url>
<driver-class>com.teradata.jdbc.TeraDriver</driver-class>
<user-name>[user name]</user-name>
<password>[password]</password>
<!-- sql to call on an existing pooled connection when it is obtained from pool -->
<check-valid-connection-sql>SELECT 1+1</check-valid-connection-sql>
</local-tx-datasource>
</datasources>
Mis pensamientos de donde puede estar la solución.
¿Hay algo que pueda hacer en un método @BeforeClass para hacer que las propiedades que el código anterior busca estén disponibles sin JBoss? ¿Tal vez de alguna manera usando el método setProperty de la clase java.util.Properties? También me gustaría usar el mismo archivo JNDI xml que JBoss lee, si es posible, para reducir los ajustes de configuración duplicados.
Hasta ahora, toda mi investigación termina con el consejo "Use Spring", pero no creo que estemos listos para abrir esa lata de gusanos todavía. No soy un experto en JBoss, pero si se necesitan más detalles de nuestra configuración de JBoss para obtener una respuesta útil, haré todo lo posible para obtenerlos, aunque es probable que necesite algunos consejos sobre dónde buscar.
Referencias de investigación de Stackoverflow:
Búsqueda de Jndi en junio usando primavera.
Fuente de datos JNDI fuera de contenedor
Otras referencias de investigación:
http://docs.oracle.com/javase/1.4.2/docs/api/java/util/Properties.html
http://docs.oracle.com/javase/jndi/tutorial/basics/prepare/initial.html
Hay una respuesta muy simple a tu problema, pero no te va a gustar: no lo hagas.
Por definición, una prueba de unidad debe verificar la funcionalidad de una sola unidad (cuyo tamaño puede variar, pero debe ser autosuficiente). Crear una configuración en la que la prueba dependa de servicios web, bases de datos, etc. es contraproducente: ralentiza sus pruebas, incluye un billón de cosas posibles que podrían salir mal (conexiones de red fallidas, cambios en los conjuntos de datos, ... ) durante la prueba, que no tienen nada que ver con el código real en el que está trabajando, y lo más importante: hace que las pruebas sean mucho más difíciles y más complicadas.
En su lugar, debe buscar formas de desacoplar el código heredado de cualquier fuente de datos, de modo que pueda sustituir fácilmente los objetos simulados o pruebas similares mientras se realiza la prueba.
Debería crear pruebas para verificar la integridad de toda su pila, pero éstas se denominan pruebas de integración y operan a un nivel más alto de abstracción. Personalmente, me gusta diferir la escritura hasta que las unidades estén en su lugar, probadas y funcionando, al menos hasta que haya llegado a un punto en el que ya no espere cambios en las llamadas de servicio y los protocolos a diario.
En su caso, la estrategia más obvia sería encapsular todas las llamadas al servicio web en una o más clases separadas, extraer una interfaz de la que pueden depender los objetos de negocios y utilizar simulacros que implementan esa misma interfaz para la prueba unitaria.
Por ejemplo, si tiene un objeto de negocio que llama a una base de datos de direcciones, debe copiar el código de búsqueda JNDI en una nueva clase de servicio llamada AddressServiceImpl
. Sus métodos públicos deben imitar todas las firmas de métodos de su fuente de datos JNDI. Esos, entonces, se extraen a la interfaz AddressService
.
Luego puede escribir una prueba de integración simple para verificar que la nueva clase funciona: llame a todos los métodos una vez y vea si obtiene los resultados adecuados. La belleza de esto es que puede proporcionar una configuración JNDI que apunta a una base de datos de prueba (en lugar de la original), que puede llenar con conjuntos de datos de prueba para asegurarse de que siempre obtenga los resultados esperados. No necesariamente necesita una instancia de JBoss para esto (aunque nunca he tenido ningún problema con la integración de eclipse): cualquier otro proveedor de JNDI debería funcionar, siempre que la fuente de datos se comporte de la misma manera. Y para ser claros: una vez que lo pruebes, olvídalo. Al menos hasta que los métodos de servicio reales cambien.
Una vez que haya verificado que el servicio es funcional, la siguiente tarea es revisar todas las clases dependientes y reemplazar las llamadas directas al origen de datos con llamadas a la interfaz de AddressService. Y a partir de ese momento, tiene una configuración adecuada para implementar pruebas unitarias en los métodos comerciales reales, sin tener que preocuparse por cosas que deberían probarse en otro lugar;)
EDITAR
Yo segundo la recomendación para code.google.com/p/mockito . ¡Realmente bueno!
Puede ejecutar sus pruebas con una implementación de InitialContext
falsa, que devuelve todo lo que necesita de llamadas a lookup(String)
.
Una herramienta de burla / falsificación que permite tales implementaciones falsas es JMockit . La implementación falsa se escribiría de la siguiente manera:
public class FakeInitialContext extends MockUp<InitialContext>
{
@Mock
public Object lookup(String name)
{
// Return whatever is needed based on "name".
}
}
Para aplicarlo a una ejecución de prueba JUnit / TestNG, agregue jmockit.jar a la ruta de clase de tiempo de ejecución (antes de junit.jar si este es el caso) y establezca la propiedad del sistema "jmockit-mocks" al nombre de la clase falsa: -Djmockit-mocks=com.whatever.FakeInitialContext
.
Por supuesto, también puede escribir verdaderas pruebas unitarias de JUnit / TestNG donde cualquier dependencia se pueda burlar fácilmente, usando la API de simulacros de "Expectativas y verificaciones".
(PD: para una divulgación completa, soy el creador del proyecto JMockit).
Si está utilizando herramientas como Git y Maven, esto se puede hacer fácilmente con ellas. Incorpore un archivo de propiedades específicas de UnitTest junto con el desarrollo lateral y qa. Utilice Maven y sus instalaciones de profile
para especificar un perfil que copie su archivo UnitTest a donde debería ir, igual que con su dev y qa cuando se ejecuta con diferentes perfiles activos.
No hay magia en esto; La primavera introduce la complejidad más que nada. Definitivamente no introduce una simplicidad como esta.
Tuve una situación muy similar con algún código heredado en JBoss AS7, para el cual la refactorización hubiera estado fuera de alcance.
Renuncié a intentar obtener la fuente de datos de JBoss, porque no es compatible con el acceso remoto a las fuentes de datos, que confirmé al intentarlo.
Sin embargo, lo ideal es que no quieras que tus pruebas de unidad dependan de una instancia de JBoss en ejecución para poder ejecutarse, y realmente no quieres que se ejecuten dentro de JBoss. Sería contrario al concepto de pruebas unitarias autocontenidas (aunque aún necesitará que la base de datos se ejecute :)).
Afortunadamente, el contexto inicial utilizado por su aplicación no tiene que provenir de una instancia de JBoss en ejecución. Después de ver este artículo al que se hace referencia en una respuesta a otra pregunta , pude crear mi propio contexto inicial, poblarlo con mi propio objeto de fuente de datos.
Esto funciona sin crear dependencias en el código porque las clases bajo prueba normalmente se ejecutan dentro del contenedor, donde simplemente hacen algo como esto para obtener el contexto proporcionado por el contenedor:
InitialContext ic = new InitialContext();
DataSource ds = (DataSource)ic.lookup(DATA_SOURCE_NAME);
No necesitan especificar ningún entorno para el constructor, ya que el contenedor ya lo ha configurado.
Para que sus pruebas unitarias se conviertan en el contenedor y proporcionen un contexto, lo crea y se une un nombre:
InitialContext ic = new InitialContext();
// Construct DataSource
OracleConnectionPoolDataSource ds = new OracleConnectionPoolDataSource();
ds.setURL("url");
ds.setUser("username");
ds.setPassword("password");
ic.bind(DATA_SOURCE_NAME, ds);
Esto debe suceder en el método @BeforeClass
cada clase de @BeforeClass
.
Ahora las clases que se están probando obtienen mi contexto inicial cuando se ejecuta en pruebas unitarias, y el contenedor cuando se implementa.