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

value y transactionManager

propagation

solo se admite Propagation.NOT_SUPPORTED y Propagation.NEVER

isolation

no

timeout

no

readOnly

no

rollbackFor y rollbackForClassName

no: use TestTransaction.flagForRollback()

noRollbackFor y noRollbackForClassName

no: use TestTransaction.flagForCommit() en su lugar

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
    HibernateUserRepository repository;
    @Autowired
    SessionFactory sessionFactory;
    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");
        User user = new User(...);
        repository.save(user);
        // Para evitar falsos positivos durante las pruebas, se requiere un reinicio manual
        sessionFactory.getCurrentSession().flush();
        assertNumUsers(count + 1);
    }
    private int countRowsInTable(String tableName) {
        return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
    }
    private void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {
    @Autowired
    lateinit var repository: 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()
        assertNumUsers(count + 1)
    }
    private fun countRowsInTable(tableName: String): Int {
        return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
    }
    private fun assertNumUsers(expected: Int) {
        assertEquals("Number of rows in the [user] table.", expected, 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)
public class ProgrammaticTransactionManagementTests extends
        AbstractTransactionalJUnit4SpringContextTests {
    @Test
    public void transactionalTest() {
        // afirmar el estado inicial en la base de datos de prueba:
        assertNumUsers(2);
        deleteFromTables("user");
        // ¡Se confirmarán los cambios en la base de datos!
        TestTransaction.flagForCommit();
        TestTransaction.end();
        assertFalse(TestTransaction.isActive());
        assertNumUsers(0);
        TestTransaction.start();
        // realizar otras acciones con la base de datos, que
        // se revertirá automáticamente una vez completada la prueba...
    }
    protected void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable( "user"));
    }
}
Kotlin
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests( ) {
    @Test
    fun transactionalTest() {
        // confirma el estado inicial en la base de datos de prueba:
        assertNumUsers(2)
        deleteFromTables("user")
        // ¡se confirmarán los cambios en la base de datos!
        TestTransaction.flagForCommit()
        TestTransaction.end()
        assertFalse(TestTransaction.isActive())
        assertNumUsers(0) TestTransaction.start()
        // realizar otras acciones con la base de datos, que
        // se revertirá automáticamente una vez completada la prueba...
    }
    protected fun assertNumUsers(expected: Int) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
    }
}

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 verifyInitialDatabaseState() {
        // 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
        // establece los datos de prueba dentro de la transacción
    @Rollback
    void modifyDatabaseWithinTransaction() {
        // 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 verifyFinalDatabaseState() {
        // 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 verifyInitialDatabaseState() {
        // 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
    @Rollback
    fun modifyDatabaseWithinTransaction() {
        // lógica usando datos de prueba y cambiando el estado de la base de datos
    }
    @AfterEach
    fun tearDownWithinTransaction() {
        // ejecuta la lógica de desmontaje dentro de la transacción
    }
    @AfterTransaction
    fun verifyFinalDatabaseState() {
        // lógica para verificar la validez de el estado final después de que se revierte 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 sessionFactory;
@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
    // Falso positivo: se lanzará una excepción tan pronto como la sesión de Hibernate
}
@Transactional
@Test(expected = ...)
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
@Transactional
@Test // ¡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(expected = ...)
fun updateWithSessionFlush() {
    updateEntityInHibernateSession()
    // finalmente se vacíe (es decir, en el código de producción)
    sessionFactory.getCurrentSession().flush()
}
// ...

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

Java
// ...
@PersistenceContext
EntityManager entityManager;
@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
    entityManager.flush();
}
// ...
Kotlin
// ...
@PersistenceContext
lateinit var entityManager: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
    entityManager.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
// ...
@Autowired
JpaPersonRepository repo;
@PersistenceContext
EntityManager entityManager;
@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
    entityManager.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 var entityManager: 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
    entityManager.flush()
    // Código de prueba que usa una 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.