Koray Tugay
Koray Tugay

Reputation: 23800

How to make DataJpaTest flush save automatically?

I have an Employee entity with the following column:

@Entity
class Employee {
  @Column(name = "first_name", length = 14)
  private String firstName;

and I have a Spring JPA Repository for it:

@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Integer> {

In test/resources/application.properties I have the following so that I use an in-memory h2 database with tables auto-generated:

spring.jpa.hibernate.ddl-auto=create
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

I was expecting this test to fail, since the firstName is longer than what is allowed:

@DataJpaTest
public class EmployeeRepositoryTest {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Test
  public void mustNotSaveFirstNameLongerThan14() {
    Employee employee = new Employee();
    employee.setFirstName("koraykoraykoray");  // 15 characters!
    employeeRepository.save(employee);
  }
}

And I was surprised to see this test was not failing, however the following does fail:

@DataJpaTest
public class EmployeeRepositoryTest {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Test
  public void testMustNotSaveFirstNameLongerThan14() {
    Employee employee = new Employee();
    employee.setFirstName("koraykoraykoray");  // 15 characters!
    employeeRepository.save(employee);
    employeeRepository.findAll();
  }
}

with the stacktrace:

Caused by: org.h2.jdbc.JdbcSQLDataException: Value too long for column "FIRST_NAME VARCHAR(14)": "'koraykoraykoray' (15)"; SQL statement:

The only difference is the second test has the additional employeeRepository.findAll(); statement, which forces Hibernate to flush as far as I understand.

This does not feel right to me, I would much rather want the test to fail immediately for save.

I can also have

@Autowired
private TestEntityManager testEntityManager;

and call

testEntityManager.flush();

but again, this does not feel correct either.. How do I make this test fail without any workaround or additional statements?

Upvotes: 11

Views: 6853

Answers (4)

Alireza Fattahi
Alireza Fattahi

Reputation: 45553

The @Commit can do the job ( it was added since 4.2)

  @Test
  @Commit
  public void mustNotSaveFirstNameLongerThan14() {
    Employee employee = new Employee();
    employee.setId(1);
    employee.setFirstName("koraykoraykoray");  // 15 characters!
    assertThrows(DataIntegrityViolationException.class, () -> {
        employeeRepository.save(employee);
    });
  }

Upvotes: 3

RomanM
RomanM

Reputation: 173

Thanks doctore for your answer, I had the similar problem as OP and your solution has helped. I decided to dig a little and figure out why it works, should someone else have this problem.

With @DataJpaTest annotated test class, your class implicitly becomes @Transactional with default propagation type Propagation.REQUIRED. That means every test method is also @Transactional with same default configuration. Now, all CRUD methods in CrudRepository are also @Transactional, but it has nothing to do with @DataJpaTest - they are transactional due to implementation. Whoa, that's a lot of transactions!

As soon as you annotate your whole class (or just a test method) with @Transactional(propagation = Propagation.NOT_SUPPORTED), your test method(s) are no longer @Transactional. However, inner methods of your test method(s), that is, CRUD operations from CrudRepository, remain transactional, meaning that they will have their own transaction scopes. Because of that, they will be committed to database immediately after execution, because by default (in Spring Boot, which users HikariCP connection pool), auto commits are turned on. Auto commits happen after every SQL query. And thus tests pass as you'd expect.

I like to visualize things, so here is the visualization of the whole process: so-63782195

I hope this was helpful. URLs from the diagram:

Upvotes: 11

user731136
user731136

Reputation:

The easiest option in your case is configure @Transactional annotation, forcing to send database all changes in your tests (it can be used only in specific ones):

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DataJpaTest
public class EmployeeRepositoryTest {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Test
  public void mustNotSaveFirstNameLongerThan14() {
    Employee employee = new Employee();
    employee.setId(1);
    employee.setFirstName("koraykoraykoray");  // 15 characters!
    assertThrows(DataIntegrityViolationException.class, () -> {
        employeeRepository.save(employee);
    });
  }

  @Test
  public void mustSaveFirstNameShorterThan14() {
    Employee employee = new Employee();
    employee.setId(1);
    employee.setFirstName("koraykor");  // 8 characters!
    employeeRepository.save(employee);
  }
}

PD: I have added a simple Integer property as PK of Employee entity due to your repository definition.

You can see the results in the following picture:

Reposity tests

Upvotes: 10

Ken Bekov
Ken Bekov

Reputation: 14015

You could use JpaRepository<T,ID> instead of CrudRepository<T,ID>. Something like:

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer>

Then you can use its saveAndFlush() method anywhere you need to send data immediately:

@Test
public void mustNotSaveFirstNameLongerThan14() {
  Employee employee = new Employee();
  employee.setFirstName("koraykoraykoray");  // 15 characters!
  employeeRepository.saveAndFlush(employee);
}

And in code where you would like to have optimization you still can use save() method.

Upvotes: 3

Related Questions