Sniady
Sniady

Reputation: 1693

JdbcTestUtils and transaction context tests

I have this problem that is causing me not to use JdbcTestUtils when running tests with a Transactional context.

If I wrap my test with @Transactional annotation and use a JdbcTemplate/DataSource it looks like the transactions used in the production code and by the JdbcTestUtils are not the same, so if I query the db in the then section the assertion fails.

This is a pseudo test:

@SpringBootTest
class TestClass {

  @Autowired
  private WebApplicationContext context;

  @BeforeEach
  void setup() {
    RestAssuredMockMvc.webAppContextSetup(context, springSecurity());
  }

  @Test
  @Transactional
  @DisplayName("test1")
  void test1(@Autowired DataSource dataSource) {
    //given
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    assertThat(countRowsInTable(jdbcTemplate, "some_tbl")).isEqualTo(1);

    //when
    // Execute app code here that adds a record to some_tbl

    //then
    assertThat(countRowsInTable(jdbcTemplate, "some_tbl")).isEqualTo(2); //<- Assertion fails. 
  }

As a workaround, I need to use Spring contexted test repositories to retrieve data in the test, but that feels like a bad idea, and I need to maintain these repositories.

Below you will find a simple spring-boot project to show the issue. https://github.com/Marek00Malik/JdbcTestUtils-sample

Upvotes: 1

Views: 1152

Answers (1)

rieckpil
rieckpil

Reputation: 12041

I cloned your project and could verify the failing test on my machine.

The reason for your test failure within the assertion of the test failingTestStoringNewObject:

assertThat(countRowsInTable(jdbcTemplate, "sample_table")).isEqualTo(1);

is that the EntityManger of JPA (the Spring Data JPA repositories use this under the hood) follows a write-behind strategy and keeps changes to JPA entities in its first-level cache (in-memory). Flushing the changes to the database does not always happen when your code executes .save() or any other database operation using the EntityManager.

The EntityManger tries to postpone flushing and therefore synchronizing its first-level cache with the database as late as possible. There is a lot to read about this and I can recommend Vlad Mihalcea's guide on this.

In your case, as you annotate your test with @Transactional, your changes will be rolled back after the test and therefore never committed to the database. You can see this in the log of the test:

2020-07-20 09:59:57.754  INFO 12648 --- [    Test worker] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@2b8ade2d testClass = SimpleObjectResourceHttpTest, testInstance = pl.code.house.example.jdbc.template.jdbctemplatetest.SimpleObjectResourceHttpTest@

For more visualization on this, you can also enable SQL logging for your tests with

spring:
  jpa:
    show-sql: on

You'll see that there is actually no INSERT statement executed, but a call to fetch the primary key of your entity.

If you temporarily use repository.saveAndFlush(newObj).toDto(); inside your SimpleObjectFacade, you'll see that it works.

I would use the SimpleObjectRepository for your integration test and use the .count() method of it. In this case, the underlying EntityManager recognizes a call to the same table and will flush its current changes before that and you'll get your expected result. Because if you use the JdbcTemplate there is no interaction with the EntityManager and therefore the EntityManager does not flush it changes as you go directly to the database with a SELECT COUNT(0) ....

UPDATE: If you don't want to make any adjustments and still use the JdbcTemplate in conjunction with Hibernate and the JpaRepositories, you could set the following flush mode for your tests only:

spring.jpa.properties.org.hibernate.flushMode=ALWAYS

This ensures to always flush the persistence context

Upvotes: 1

Related Questions