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 ).
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 elTimeOut
reglas de JUnit 4Métodos
assertTimeoutPreemptively(...)
de JUnit Jupite en la claseorg.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.
Atributo | Admitido para prueba transacciones impulsadas |
---|---|
|
sí |
|
solo se admite |
|
no |
|
no |
|
no |
|
no: use |
|
no: use |
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:
@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"));
}
}
@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
.
@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"));
}
}
@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.
@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:
@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
}
}
@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
}
}
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:
// ...
@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();
}
// ...
// ...
@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:
// ...
@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();
}
// ...
// ...
@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()
}
// ...
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
.
// ...
@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...
}
//...
// ...
@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.
GO TO FULL VERSION