En el marco TestContext, las transacciones se administran utilizando TransactionalTestExecutionListener, que está configurado de forma predeterminada, incluso si la anotación @TestExecutionListeners no está declarada explícitamente en su clase de prueba. Sin embargo, para habilitar el soporte de transacciones, debe configurar un bean PlatformTransactionManager en ApplicationContext, que está cargado con la semántica de la anotación @ContextConfiguration ( se proporcionarán más detalles más adelante). Además, debe declarar la anotación @Transactional de Spring para sus pruebas, ya sea a nivel de clase o de método.

Transacciones basadas en pruebas

Las transacciones basadas en pruebas son transacciones que se controlan de forma declarativa usando TransactionalTestExecutionListener o mediante programación usando TestTransaction (que se describe más adelante). Dichas transacciones no deben confundirse con transacciones administradas por Spring (administradas directamente a través del marco Spring en el ApplicationContext cargado para las pruebas) o transacciones administradas por aplicaciones (administradas mediante programación en el código de la aplicación al que llaman las pruebas). ). Las transacciones administradas por Spring y por aplicaciones generalmente implican transacciones basadas en pruebas. Sin embargo, se debe tener precaución si Spring o las transacciones administradas por aplicaciones se configuran utilizando cualquier tipo de propagación que no sea REQUIRED o SUPPORTS (consulte la subsección para obtener más detalles) propagación de transacciones ).

Tiempos de espera de anticipación y transacciones basadas en pruebas

Se debe tener precaución al utilizar cualquier forma de tiempo de espera de anticipación de un marco de prueba en combinación con transacciones basadas en pruebas de Spring.

Específicamente, el soporte de pruebas de Spring vincula el estado de la transacción al hilo actual (a través de la variable java.lang.ThreadLocal) antes de llamar al método de prueba actual. Si el marco de prueba llama al método de prueba actual en un nuevo subproceso para admitir un tiempo de espera preventivo, entonces cualquier acción realizada en el método de prueba actual no será llamada en la transacción basada en prueba. Por lo tanto, el resultado de dichas acciones no se puede revertir en una transacción gestionada de prueba. En su lugar, dichas acciones se enviarán a un almacenamiento persistente, como una base de datos relacional, incluso si Spring revierte correctamente la transacción basada en pruebas.

Los casos en los que esto puede ocurrir pueden incluir, pero no son limitado a los que se muestran a continuación.

  • Admite la anotación @Test(timeout = ...) y el TimeOut reglas de JUnit 4

  • Métodos assertTimeoutPreemptively(...) de JUnit Jupite en la clase org.junit.jupiter.api.Assertions

  • Soporte para la anotación @Test(timeOut = ...) de TestNG

Activación y desactivación de transacciones

Anotar un método de prueba con @Transactional hace que la prueba se ejecute en una transacción, que de forma predeterminada se revierte automáticamente cuando se completa la prueba. . Si una clase de prueba está marcada con la anotación @Transactional, entonces cada método de prueba en la jerarquía de clases se ejecuta dentro de una transacción. Los métodos de prueba que no están anotados con @Transactional (a nivel de clase o método) no se ejecutan dentro de una transacción. Tenga en cuenta que la anotación @Transactiona.l no es compatible con los métodos del ciclo de vida de prueba; por ejemplo, métodos anotados con las anotaciones @BeforeAll, @BeforeEach de JUnit Júpiter, etc. Además, las pruebas marcadas con la anotación @Transactional pero que tienen el atributo propagation establecido en NOT_SUPPORTED o NEVER no lo son. ejecutado como parte de una transacción.

en su lugar
Tabla 1. Soporte para atributos anotados con @Transactional
Atributo Admitido para prueba transacciones impulsadas

valor y transactionManager

propagación

solo se admite Propagation.NOT_SUPPORTED y Propagation.NEVER

aislamiento

no

tiempo de espera

no

solo lectura

no

rollbackFor y rollbackForClassName

no: use TestTransaction.flagForRollback()

noRollbackFor y noRollbackForClassName

no: use TestTransaction.flagForCommit()

Métodos de ciclo de vida a nivel de método; por ejemplo, métodos anotados con @BeforeEach o @AfterEach de JUnit Jupiter: ejecútelo dentro de una transacción basada en pruebas. Por otro lado, los métodos del ciclo de vida a nivel de suite y clase, por ejemplo, métodos anotados con @BeforeAll o @AfterAll de JUnit Jupiter y métodos anotados con @ BeforeSuite, @AfterSuite, @BeforeClass o @AfterClass de TestNG - ne se ejecutan dentro un marco de transacciones basado en pruebas.

Si desea ejecutar código en un método de ciclo de vida a nivel de paquete o de clase dentro de una transacción, puede inyectar el PlatformTransactionManager apropiado en un clase de prueba y luego úsela con TransactionTemplate para la gestión de transacciones programáticas.

Tenga en cuenta que AbstractTransactionalJUnit4SpringContextTests y AbstractTransactionalTestNGSpringContextTests están preconfigurados para admitir transacciones a nivel de clase.

El siguiente ejemplo demuestra un escenario común para escribir una prueba de integración para un UserRepository basado en Hibernar:

Java
@SpringJUnitConfig(TestConfig.class) @Transactional class HibernateUserRepositoryTests { @Autowired Repositorio HibernateUserRepository; @Autowired SessionFactory sesiónFactory; JdbcTemplate jdbcTemplate; @Autowired void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Test void createUser() { // realiza un seguimiento del estado inicial en la base de datos de prueba: final int count = countRowsInTable("user"); Usuario usuario = nuevo Usuario(...); repositorio.save(usuario); // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual sessionFactory.getCurrentSession().flush(); afirmarNumUsers(cuenta + 1); } privado int countRowsInTable(String tableName) { return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); } private void afirmarNumUsers(int esperado) { afirmarEquals("Número de filas en la tabla [usuario].", esperado, countRowsInTable("usuario")); } }
Kotlin
@SpringJUnitConfig(TestConfig::class) @Transactional class HibernateUserRepositoryTests { @Autowired lateinit var repositorio: HibernateUserRepository @Autowired lateinit var sessionFactory: SessionFactory lateinit var jdbcTemplate: JdbcTemplate @Autowired fun setDataSource(dataSource: DataSource) { this.jdbcTemplate = JdbcTemplate(dataSource) } @Test fun createUser() { // realiza un seguimiento del estado inicial en base de datos de prueba: val count = countRowsInTable("user") val user = User() repository.save(user) // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual sessionFactory.getCurrentSession().flush() afirmarNumUsers(count + 1) } diversión privada countRowsInTable(nombreTabla: Cadena): Int { return JdbcTestUtils.countRowsInTable(jdbcTemplate, nombreTabla) } diversión privada afirmarNumUsers(esperado: Int) { afirmarEquals("Número de filas en la tabla [usuario].", esperado , countRowsInTable("user ")) } }

Como se explica en la sección "Reversión de transacciones y lógica de confirmación", no es necesario limpiar el base de datos después de ejecutar el método createUser (), ya que cualquier cambio realizado en la base de datos se revierte automáticamente mediante TransactionalTestExecutionListener.

Lógica para revertir y confirmar transacciones

De forma predeterminada, las transacciones de prueba se revierten automáticamente una vez que se completa la prueba; sin embargo, la lógica para confirmar y revertir transacciones se puede configurar de forma declarativa utilizando las anotaciones @Commit y @Rollback. Para obtener más información, consulte las entradas relacionadas en la sección sobre Soporte de anotaciones.

Gestión de transacciones del programa

Puede interactuar con transacciones basadas en pruebas mediante programación utilizando métodos estáticos en TestTransaction. Por ejemplo, puede utilizar TestTransaction en los métodos de prueba, antes y después para iniciar o finalizar la transacción basada en prueba actual, o para configurar la transacción basada en prueba actual para revertirla o confirmarla. La compatibilidad con TestTransaction está disponible automáticamente siempre que el detector TransactionalTestExecutionListener esté habilitado.

El siguiente ejemplo demuestra algunas de las capacidades de TestTransaction . Para obtener más información, consulte el javadoc en TestTransaction.

Java
@ContextConfiguration(classes = TestConfig.class) clase pública ProgrammaticTransactionManagementTests extends AbstractTransactionalJUnit4SpringContextTests { @Test public void transactionalTest() { // afirmar el estado inicial en la base de datos de prueba: afirmarNumUsers(2); eliminarDeTables("usuario"); // ¡Se confirmarán los cambios en la base de datos! TestTransaction.flagForCommit(); TestTransaction.end(); afirmarFalse(TestTransaction.isActive()); afirmarNumUsuarios(0); TestTransaction.start(); // realizar otras acciones con la base de datos, que // se revertirá automáticamente una vez completada la prueba... } protected void afirmarNumUsers(int esperado) { afirmarEquals("Número de filas en la tabla [usuario].", esperado , countRowsInTable( "usuario")); } }
Kotlin
@ContextConfiguration(classes = [TestConfig::class]) class ProgrammaticTransactionManagementTests: AbstractTransactionalJUnit4SpringContextTests( ) { @Test fun transaccionalTest() { // confirma el estado inicial en la base de datos de prueba: afirmarNumUsers(2) deleteFromTables("usuario") // ¡se confirmarán los cambios en la base de datos! TestTransaction.flagForCommit() TestTransaction.end() afirmarFalse(TestTransaction.isActive()) afirmarNumUsers(0) TestTransaction.start() // realizar otras acciones con la base de datos, que // se revertirá automáticamente una vez completada la prueba. .. } protected fun afirmarNumUsers(esperado: Int) { afirmarEquals("Número de filas en la tabla [usuario].", esperado, countRowsInTable("usuario")) } }

Ejecutar código fuera de una transacción

A veces es posible que desee ejecutar cierto código antes o después de un método de prueba transaccional, pero fuera del contexto transaccional, por ejemplo, para verificar el estado inicial. de la base de datos antes de ejecutar una prueba, o para verificar el comportamiento esperado de un método de prueba transaccional, se confirma después de la ejecución de la prueba (si la prueba se configuró para confirmar una transacción). TransactionalTestExecutionListener admite las anotaciones @BeforeTransaction y @AfterTransactio.n solo para esos escenarios. Puede anotar cualquier método void en una clase de prueba o cualquier método void predeterminado en una interfaz de prueba con una de estas anotaciones, y el oyente TransactionalTestExecutionListener se asegurará de que su método "previo a la transacción" o "posterior a la transacción" se haya ejecutado en el momento correcto.

Cualquier método "antes" (por Por ejemplo, se ejecutan los métodos marcados con la anotación @BeforeEach de JUnit Jupiter) y cualquier método "después" (por ejemplo, los métodos marcados con la anotación @AfterEach de JUnit Jupiter) dentro de una transacción. Además, los métodos anotados con la anotación @BeforeTransaction o @AfterTransaction no se ejecutan para los métodos de prueba que no están configurados para ejecutarse dentro de una transacción.

Configuración crear un administrador de transacciones

TransactionalTestExecutionListener espera que se defina un bean PlatformTransactionManager en ApplicationContext de Spring para la prueba. Si hay varias instancias de PlatformTransactionManager dentro de una prueba ApplicationContext, puede declarar un calificador usando @Transactional("myTxMgr") o La anotación @Transactional (transactionManager = "myTxMgr") o TransactionManagementConfigurer se puede implementar mediante una clase marcada con la anotación @Configuration. Consulte javadoc en TestContextTransactionUtils.retrieveTransactionManager() para obtener más detalles sobre el algoritmo utilizado para la transacción de búsqueda. manager en el ApplicationContext de la prueba.

Mostrando todas las anotaciones relacionadas con transacciones

El siguiente ejemplo basado en JUnit Jupiter muestra un escenario de prueba de integración ficticio que resalta todas las anotaciones asociadas con las transacciones. El ejemplo no pretende demostrar las mejores prácticas, sino que sirve como demostración de cómo se pueden utilizar estas anotaciones. Para obtener más información y ejemplos de configuración, consulte la sección Soporte de anotaciones. Gestión de transacciones para la anotación @Sql contiene un ejemplo adicional utilizando el anotación @Sql para ejecutar un script SQL declarativo con semántica de reversión de transacciones predeterminada. El siguiente ejemplo muestra las anotaciones correspondientes:

Java
@SpringJUnitConfig @Transactional(transactionManager = "txMgr") @Commit class FictitiousTransactionalTest { @BeforeTransaction void verificarInitialDatabaseState() { // lógica para verificar la validez del estado inicial antes de iniciar la transacción } @BeforeEach void setUpTestDataWithinTransaction() { // establece los datos de prueba dentro de la transacción } @Test // anula la configuración de anotación @Commit en el nivel de clase @Rollback void modificarDatabaseWithinTransaction() { // lógica usando datos de prueba y cambiando el estado de la base de datos } @AfterEach void tearDownWithinTransaction() { // ejecuta la lógica de desmontaje dentro de la transacción } @AfterTransaction void verificarFinalDatabaseState() { // lógica para verificar la validez de el estado final después de que se revierte la transacción } }
Kotlin
@SpringJUnitConfig @Transactional(transactionManager = " txMgr") @Commit class FictitiousTransactionalTest { @BeforeTransaction fun verificarInitialDatabaseState( ) { // lógica para verificar la validez del estado inicial antes de iniciar la transacción } @BeforeEach fun setUpTestDataWithinTransaction() { // establecer los datos de prueba dentro de la transacción } @Test // anula la configuración de anotación @Commit a nivel de clase @Rollback divertido modificarDatabaseWithinTransaction() { // lógica que usa datos de prueba y cambia el estado de la base de datos } @AfterEach divertido TearDownWithinTransaction() { // ejecuta la lógica de desmontaje dentro de la transacción } @AfterTransaction fun verificarFinalDatabaseState() { // lógica para verificar la validez del estado final después de revertir la transacción } }
Evitar falsos positivos al probar el código ORM

Cuando pruebas código de aplicación que manipula el estado de una sesión de Hibernate o contexto de persistencia JPA, asegúrese de restablecer la unidad básica de trabajo en los métodos de prueba que ejecutan este código. No restablecer la unidad básica de trabajo puede generar falsos positivos: la prueba pasará, pero en un entorno de producción real el mismo código generará una excepción. Tenga en cuenta que esto se aplica a cualquier sistema ORM que almacene una unidad de trabajo en la memoria. En el siguiente caso de prueba basado en Hibernate, un método muestra un falso positivo, mientras que otro método abre correctamente los resultados del restablecimiento de la sesión:

Java
// ... @Autowired SessionFactory sesiónFactory; @Transactional @Test // ¡no se espera ninguna excepción! public void falsePositive() { updateEntityInHibernateSession(); // Falso positivo: se lanzará una excepción tan pronto como la sesión de Hibernate // finalmente se vacíe (es decir, en el código de producción) } @Transactional @Test(esperado = ...) public void updateWithSessionFlush() { updateEntityInHibernateSession(); // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual sessionFactory.getCurrentSession().flush(); } // ...
Kotlin
// ... @Autowired lateinit var sessionFactory: SessionFactory @ @Test transaccional // ¡no se espera ninguna excepción! fun falsePositive() { updateEntityInHibernateSession() // Falso positivo: se lanzará una excepción tan pronto como la sesión de Hibernate // finalmente se vacíe (es decir, en el código de producción) } @Transactional @Test(esperado = ...) fun updateWithSessionFlush( ) { updateEntityInHibernateSession() // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual sessionFactory.getCurrentSession().flush() } // ...

El siguiente ejemplo muestra métodos de mapeo para JPA:

Java
// ... @PersistenceContext EntityManagerentityManager; @Transactional @Test // ¡no se espera ninguna excepción! public void falsePositive() { updateEntityInJpaPersistenceContext(); // Falso positivo: se lanzará una excepción tan pronto como // el EntityManager de JPA finalmente se vacíe (es decir, en el código de producción) } @Transactional @Test(expected = ...) public void updateWithEntityManagerFlush() { updateEntityInJpaPersistenceContext(); // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual entidadManager.flush(); } // ...
Kotlin
// ... @PersistenceContext lateinit varentityManager:EntityManager @Transactional @Test // ¡no se espera ninguna excepción! fun falsePositive() { updateEntityInJpaPersistenceContext() // Falso positivo: se lanzará una excepción tan pronto como // el EntityManager de JPA finalmente se vacíe (es decir, en el código de producción) } @Transactional @Test(expected = ...) void updateWithEntityManagerFlush () { updateEntityInJpaPersistenceContext() // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual de entidadManager.flush() } // ...
Prueba de devoluciones de llamadas del ciclo de vida de entidades ORM

Similar a la nota sobre cómo evitar falsos positivos Al probar el código ORM, si su aplicación utiliza devoluciones de llamadas del ciclo de vida de la entidad (también conocidas como escuchas de entidades), asegúrese de que los métodos de prueba que ejecutan ese código restablezcan la unidad de trabajo subyacente. Si no se restablece o borra una unidad de trabajo básica, es posible que no se llamen a ciertas devoluciones de llamada del ciclo de vida.

Por ejemplo, cuando se utilizan devoluciones de llamada JPA con anotaciones @PostPersist, @PreUpdate y @PostUpdate no se confirmarán a menos que la función entityManager.flush() se llame después de guardar o actualización de la entidad. Del mismo modo, si una entidad ya está adjunta a la unidad de trabajo actual (asociada con el contexto de persistencia actual), intentar recargar la entidad no activará la devolución de llamada de la anotación @PostLoad a menos que la función entityManager se llama antes de intentar recargar la entidad .clear().

El siguiente ejemplo muestra cómo restablecer el EntityManager para que las devoluciones de llamada al @PostPersist Se garantiza que la anotación fallará cuando se guarde la entidad. La entidad Person utilizada en el ejemplo tenía un detector de entidad registrado con un método de devolución de llamada marcado con la anotación @PostPersist.

Java
            // ... repositorio @Autowired JpaPersonRepository; @PersistenceContext EntityManager entidadManager;
                @Transactional @Test void savePerson() { // EntityManager#persist(...) da como resultado @PrePersist
                pero no @PostPersist repo.save(new Person("Jane")); // La devolución de llamada con la anotación
                @PostPersist requiere un vaciado manual entidadManager.flush(); // Código de prueba que usa una
                devolución de llamada con la anotación @PostPersist // fue llamado... } // ...
Kotlin
// ...@Autowired lateinit var repo: JpaPersonRepository @PersistenceContext lateinit varentityManager: EntityManager @Transactional @Test fun savePerson() { // EntityManager#persist(...) da como resultado @PrePersist , pero no @PostPersist repo.save(Person("Jane")) // La devolución de llamada con la anotación @PostPersist requiere un vaciado manual entidadManager.flush() // El código de prueba que usa la devolución de llamada con la anotación @PostPersist // fue llamado... } // ...

Ver JpaEntityListenerTests en Spring Framework Test Suite para ver ejemplos de trabajo que utilizan todas las devoluciones de llamada del ciclo de vida de JPA.