У фреймворку TestContext транзакції керуються за допомогою TransactionalTestExecutionListener, який налаштований за замовчуванням, навіть якщо анотація @TestExecutionListeners явно не оголошена у своєму тестовому класі. Однак, щоб активувати засоби підтримки транзакцій, необхідно налаштувати бін PlatformTransactionManager у ApplicationContext, який завантажується з семантикою анотації @ContextConfiguration (більш детальна інформація буде представлена пізніше). Крім того, потрібно оголосити для твоїх тестів анотацію @Transactional зі Spring або на рівні класу, або на рівні методу.

Транзакції, керовані тестуванням

Керовані тестами транзакції — це транзакції, які управляються декларативно за допомогою TransactionalTestExecutionListener або програмно за допомогою TestTransaction (описано далі). Не слід плутати такі транзакції з транзакціями, керованими Spring (безпосередньо через фреймворк Spring у ApplicationContext, завантаженому для тестів) або транзакціями, керованими додатком (програмно в коді програми, що викликається тестами). Керовані Spring та керовані додатком транзакції зазвичай беруть участь у керованих тестами транзакціях. Однак слід бути обережним, якщо транзакції, керовані Spring або додатком, налаштовані з використанням будь-якого типу поширення, крім REQUIRED або SUPPORTS (докладніше див. опис у підрозділі, присвяченому розповсюдженню транзакцій).

Запобіжний час очікування та транзакції, керовані тестами

Необхідно дотримуватись пильності при використанні будь-якої форми попереджувального часу очікування з тестового фреймворку у поєднанні з керованими тестами транзакціями Spring.

Засоби підтримки тестування зі Spring прив'язують стан транзакції до поточного потоку (через змінну java.lang.ThreadLocal ) перед викликом поточного тестового методу. Якщо тестовий фреймворк викликає поточний метод тестування в новому потоці для підтримки запобіжного часу очікування, то будь-які дії, що виконуються в поточному методі тестування, не будуть викликані в транзакції, що керується тестом. Отже, результат будь-яких таких дій не можна буде відкотити під час транзакції, керованої тестом. Навпаки, такі дії будуть зафіксовані в постійному сховищі — наприклад, в реляційній базі даних — навіть якщо керована тестами транзакція буде правильно відкочена Spring. Випадки, в яких це може статися, наведено нижче.

  • Підтримка анотації @Test(timeout = …) та правила TimeOut з JUnit 4

  • Методи assertTimeoutPreemptively(…) з JUnit Jupite у класі org.junit.jupiter.api.Assertions

  • Підтримка анотації @Test(timeOut = …) з TestNG

Активація та дезактивація транзакцій

Анотування тестового методу за допомогою @Transactional призводить до того, що тест запускається в транзакції, яка за замовчуванням автоматично відкочується після завершення тесту. Якщо тестовий клас позначений анотацією @Transactional, то кожен тестовий метод в ієрархії цього класу виконується в рамках транзакції. Тестові методи, які не анотовані @Transactional (на рівні класу або методу), не виконуються в рамках транзакції. Зверни увагу, що анотація @Transactional не підтримується для методів життєвого циклу тесту — наприклад, методів, анотованих за допомогою анотацій @BeforeAll, @BeforeEach з JUnit Jupiter і т.д. До того ж, тести, позначені анотацією @Transactional, але мають атрибут propagation, встановлений у NOT_SUPPORTED або NEVER, не виконуються в межах транзакції.

Таблиця 1. Підтримка атрибутів, анотованих @Transactional
Атрибут Підтримується для транзакцій, керованих тестами

value та transactionManager

так

propagation

підтримуються тільки Propagation.NOT_SUPPORTED та Propagation.NEVER

isolation

ні

timeout

ні

readOnly

ні

rollbackFor та rollbackForClassName

ні: замість цього використовуй TestTransaction.flagForRollback()

noRollbackFor та noRollbackForClassName

ні: замість цього використовуй TestTransaction.flagForCommit()

Методи життєвого циклу на рівні методів — наприклад, методи, анотовані за допомогою @BeforeEach або @AfterEach з JUnit Jupiter — виконуються в межах керованої тестами транзакції. З іншого боку, методи життєвого циклу на рівні комплекту та класу — наприклад, методи, анотовані @BeforeAll або @AfterAll з JUnit Jupiter та методи, анотовані @BeforeSuite, @AfterSuite, @BeforeClass або @AfterClass з TestNG — не виконуються в межах керованої тестами транзакції.

Якщо потрібно запустити код у методі життєвого циклу на рівні комплекту або класу в рамках транзакції, то можна впровадити відповідний PlatformTransactionManager до тестового класу, а потім використовувати його з TransactionTemplate для програмного управління транзакціями.

Зверни увагу, що AbstractTransactionalJUnit4SpringContextTests і попередньо налаштовані на підтримку транзакцій на рівні класів.

У наступному прикладі продемонстровано загальний сценарій написання інтеграційного тесту для UserRepository на основі Hibernate:

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() {
        // відстежуємо початковий стан у тестовій базі даних:
        final int count = countRowsInTable("user");
        User user = new User(...);
        repository.save(user);
        // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
        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() {
        // відстежуємо початковий стан у тестовій базі даних:
        val count = countRowsInTable("user")
        val user = User()
        repository.save(user)
        // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
        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 "))
        }
        }
    

Як пояснюється в розділі "Логіка роботи при відкаті та фіксації транзакцій", немає необхідності очищати базу даних після виконання методу createUser (), оскільки будь-які зміни, внесені до бази даних, автоматично відкочуються TransactionalTestExecutionListener.

Логіка роботи при відкаті та фіксації транзакцій

За замовчуванням тестові транзакції автоматично відкочуються після завершення тесту; однак логіку роботи при фіксації та відкаті транзакцій можна налаштувати декларативно за допомогою анотацій @Commit та @Rollback. Докладніше див. відповідні записи в розділі, присвяченому підтримці анотацій.

Керування програмними транзакціями

Ти можеш взаємодіяти з керованими тестами транзакціями програмно, використовуючи статичні методи у TestTransaction. Наприклад, можна використовувати TestTransaction у тестових методах, методах "перед" і "після" для запуску або завершення поточної керованої тестом транзакції або для конфігурування поточної керованої тестом транзакції на відкат або фіксацію. Підтримка TestTransaction автоматично доступна завжди, коли включений слухач TransactionalTestExecutionListener.

У цьому прикладі показано деякі можливості TestTransaction. Докладнішу інформацію див. у javadoc TestTransaction.

Java

@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
        AbstractTransactionalJUnit4SpringContextTests {
    @Test
    public void transactionalTest() {
        // підтверджуємо початковий стан у тестовій базі даних:
        assertNumUsers(2);
        deleteFromTables("user");
        // Зміни в базі даних будуть зафіксовані!
        TestTransaction.flagForCommit();
        TestTransaction.end();
        assertFalse(TestTransaction.isActive());
        assertNumUsers(0);
        TestTransaction.start();
        // виконувати інші дії з базою даних, які будуть
        // автоматично відкачені після завершення тесту...
    }
    protected void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable( "user"));
    }
}
Kotlin

@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContext ) {
    @Test
    fun transactionalTest() {
        // підтверджуємо початковий стан у тестовій базі даних:
        assertNumUsers(2)
        deleteFromTables("user")
        // зміни у базі даних будуть зафіксовані!
        TestTransaction.flagForCommit()
        TestTransaction.end()
        assertFalse(TestTransaction.isActive())
        assertNumUsers(0) TestTransaction.start()
        // виконуємо інші дії з базою даних, які будуть
        // автоматично відкачені після завершення тесту...
    }
    protected fun assertNumUsers(expected: Int) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
    }
}

Виконання коду поза транзакцією

Іноді може знадобитися виконати певний код перед або після транзакційного тестового методу, але поза транзакційним контекстом — наприклад, для перевірки початкового стану бази даних перед запуском тесту або для перевірки очікуваної поведінки транзакційної фіксації після виконання тесту (якщо тест було налаштовано на фіксацію транзакції). TransactionalTestExecutionListener підтримує анотації @BeforeTransaction та @AfterTransactio.n саме для таких сценаріїв. Ти можеш анотувати будь-який void метод у тестовому класі або будь-який void метод за замовчуванням у тестовому інтерфейсі однієї з цих анотацій, а слухач TransactionalTestExecutionListener забезпечить, щоб твій метод "перед транзакцією" або "після транзакції" був запущений у потрібний час.

Будь-які методи "перед" (наприклад, методи, позначені анотацією @BeforeEach з JUnit Jupiter) та будь-які методи "після" (наприклад, методи, позначені анотацією @AfterEach з JUnit Jupiter) виконуються в межах транзакції. До того ж, методи, анотовані за допомогою анотації @BeforeTransaction або @AfterTransaction, не виконуються для тестових методів, які не налаштовані на виконання в межах транзакції.

Налаштування диспетчера транзакцій

TransactionalTestExecutionListener очікує, що в ApplicationContext зі Spring для тесту буде визначено бін PlatformTransactionManager. Якщо існує кілька екземплярів PlatformTransactionManager в межах ApplicationContext тесту, можна оголосити кваліфікатор, використовуючи анотацію @Transactional("myTxMgr") або @Transactional (transactionManager = "myTxMgr"), або TransactionManagementConfigurer може бути реалізований класом, позначеним анотацією @Configuration. Звернися до javadoc за TestContextTransactionUtils.retrieveTransactionManager() для ознайомлення з більш детальною інформацією про алгоритм, який використовується для пошуку диспетчера транзакцій у ApplicationContext тесту.

Демонстрація всіх анотацій, пов'язаних з транзакціями

У наступному прикладі на основі JUnit Jupiter відображено фіктивний сценарій інтеграційного тестування, в якому виділено всі анотації, пов'язані з транзакціями. Приклад не є демонстрацією передової практики, а скоріше є демонстрацією того, як їх можна використовувати. Додаткову інформацію та приклади конфігурації див. у розділі, присвяченому підтримці анотацій. Керування транзакціями для анотації @Sql містить додатковий приклад, який використовує анотацію @Sql для виконання декларативного SQL-скрипту з семантикою відкату транзакції за замовчуванням. У наступному прикладі показані відповідні анотації:

Java

@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
    @BeforeTransaction
    void verifyInit
        // логіка перевірки достовірності початкового стану перед запуском транзакції
    }
    @BeforeEach
    void setUpTestDataWithinTransaction() {
        // встановлюємо тестові дані в рамках транзакції }
    @Test
        // перевизначає налаштування анотації @Commit на рівні класу
    @Rollback
    modifyDatabaseWithinTransaction() {
        // логіка, що використовує тестові дані та змінює стан бази даних
    }
    @AfterEach void tearDownWithinTransaction() {
        // запускаємо логіку "руйнування (tear down)" всередині транзакції
    }
    @AfterTransaction
    void verifyFinalDatabaseState() {
        // логіка перевірки достовірності кінцевого стану після транзакції
    }
}
Kotlin

@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
    @BeforeTransaction
    fun verifyInitialDatabaseState() {
        // логіка перевірки достовірності початкового стану перед запуском транзакції
    }
    @BeforeEach fun setUpTestDataWithinTransaction() {
        // встановлюємо тестові дані в рамках транзакції }
    @Test
        // перевизначає налаштування інструкції @Commit на рівні класу
    @Rollback
    fun modifyDatabaseWithinTransaction() {
        // логіка, що використовує тестові дані та змінює стан бази даних
    }
    @AfterEach
    fun tearDownWithinTransaction() {
        // запускаємо логіку "руйнування (tear down)" всередині транзакції
    }
    @AfterTransaction
    fun verifyFinalDatabaseState() {
        // логіка перевірки достовірності кінцевого стану після транзакції
}
Уникаємо помилкових спрацьовувань при тестуванні ORM-коду

Коли ти тестуєш код програми, який маніпулює станом сесії Hibernate або контексту сталості JPA, переконайся, що скидаєш базову одиницю роботи в тестових методах, які виконують цей код. Відсутність скидання базової одиниці роботи може призвести до помилкових спрацьовувань: твій тест проходитиме, але в реальному виробничому середовищі той же код генеруватиме виняток. Зверни увагу, що це стосується будь-якої ORM-системи, яка зберігає одиницю роботи в пам'яті. У наступному тестовому прикладі на основі Hibernate один метод демонструє хибне спрацювання, а інший метод правильно відкриває результати скидання сесії:

Java

// ...
@Autowired
SessionFactory sessionFactory;
@Transactional
@Test // очікуваного виключення немає!
public void falsePositive() {
    updateEntityInHibernateSession();
    // Хибне спрацювання: виняток буде згенеровано, як тільки сесія Hibernate
    // буде остаточно скинута (тобто у виробничому коді)
}
@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
    updateEntityInHibernateSession
    // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
    sessionFactory.getCurrentSession().flush();
}
// ...
Kotlin

// ...
@Autowired lateinit var sessionFactory: SessionFactory
@Transactional
@Test
// очікуваного виключення немає!
fun falsePositive() {
    updateEntityInHibernateSession()
    // Помилкове спрацювання: виняток буде згенеровано, як тільки сесія Hibernate
    // буде остаточно скинута (тобто у виробничому коді)
}
@Transactional
@Test(expected = ...)
fun updateWithSessionFlush() {
    updateEntityInHibernateSession()
    // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
    sessionFactory.getCurrentSession().flush()
}
// ...

У цьому прикладі показані методи зіставлення для JPA:

Java

// ...
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test // очікуваного виключення немає!
public void falsePositive() {
    updateEntityInJpaPersistenceContext();
    // Помилкове спрацювання: виняток буде згенеровано, як тільки
    // EntityManager з JPA буде остаточно скинутий (тобто у виробничому коді)
}
@Transactional
@Test(expected = ...)
public void
    updateWithEntityManagerFlush() {
    updateEntityInJpaPersistenceContext
    // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
    entityManager.flush();
}
// ...
Kotlin

// ...
@PersistenceContext
lateinit var entityManager:EntityManager
@Transactional
@Test // очікуваного виключення немає!
fun falsePositive() {
    updateEntityInJpaPersistenceContext()
    // Помилкове спрацювання: виняток буде згенеровано, як тільки
    // EntityManager з JPA буде остаточно скинутий (тобто у виробничому коді)
}
@Transactional
@Test(expected = ...)
void updateWithEntityManagerFlush() {
    updateEntityInJpaPersistenceContext()
    // Щоб уникнути помилкових спрацьовувань при тестуванні потрібно ручне скидання
    entityManager.flush()
}
// ...
Тестування зворотних викликів життєвого циклу ORM-сутностей

По аналогії з приміткою про те, як уникнути хибних спрацьовувань при тестуванні ORM-коду, якщо твоя програма використовує зворотні виклики життєвого циклу сутностей (також відомі як слухачі сутностей), переконайся, що в тестових методах, які виконують цей код, базова одиниця роботи скидається. Неможливість скинути або очистити базову одиницю роботи може призвести до того, що певні зворотні виклики життєвого циклу не будуть викликані.

Наприклад, під час використання JPA зворотні виклики з анотаціями @PostPersist, @PreUpdate та @PostUpdate не будуть здійснені, якщо не викличеться функція entityManager.flush() після збереження чи оновлення сутності. Аналогічно, якщо сутність вже прикріплена до поточної одиниці роботи (пов'язана з поточним контекстом сталості), спроба перезавантажити сутність не призведе до зворотного виклику анотації @PostLoad, якщо перед спробою перезавантаження сутності не буде викликана функція entity .clear().

У наступному прикладі показано, як скинути EntityManager, щоб зворотні виклики анотації @PostPersist були гарантовано здійснені, коли сутність збережеться. Для сутності Person, яка використовується в прикладі, був зареєстрований слухач сутностей із методом зворотного виклику, позначеним анотацією @PostPersist.

Java

// ...
@Autowired
JpaPersonRepository repo;
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test void savePerson() {
    // EntityManager#persist(...) призводить до @PrePersist, але не @PostPersist
    repo.save(new Person("Jane"));
    // Для здійснення зворотного виклику з анотацією @PostPersist потрібно ручне скидання
    entityManager.flush();
    // Тестовий код, який використовує зворотній виклик з анотацією @PostPersist
    // був викликаний...
}
// ...
Kotlin

// ...
@Autowired
lateinit var repo: JpaPersonRepository
@PersistenceContext
lateinit var entityManager: EntityManager
@Transactional
@Test
fun savePerson() {
    // EntityManager#persist(...) призводить до @PrePersist, але не @PostPersist
    repo.save(Person("Jane"))
    // Для здійснення зворотного виклику з анотацією @PostPersist потрібно ручне скидання
    entityManager.flush()
    // Тестовий код, який використовує зворотний виклик з анотацією @PostPersist
    // був викликаний ...
}
// ...

Див. JpaEntityListenerTests у тестовому комплекті Spring Framework для ознайомлення з робочими прикладами, в яких використовуються всі зворотні виклики життєвого циклу JPA.