19

I'am trying to do a simple Integration test using Spring Boot Test in order to test the e2e use case. My test does not work because I'am not able to make the repository saving data, I think I have a problem with spring contexts ...

This is my Entity:

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    @Id
    private int id;
    private String name;
}

This is the Person repository:

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {
}

The Person service:

@Service
public class PersonService {

    @Autowired
    private PersonRepository repository;

    public Person createPerson(int id,String name) {
       return repository.save(new Person(id, name));
    }

    public List<Person> getPersons() {
      return repository.findAll();
    }
}

The Person Controller:

@RequestMapping
@RestController
public class PersonController {

  @Autowired
  private PersonService personService;

  @RequestMapping("/persons")
  public List<Person> getPersons() {
      return personService.getPersons();
  }

}

The main Application class:

@SpringBootApplication
public class BootIntegrationTestApplication {

  public static void main(String[] args) {
    SpringApplication.run(BootIntegrationTestApplication.class, args);
  }
}

The application.properties file:

spring.datasource.url= jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

And the Test:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BootIntegrationTestApplicationTests {

    @Autowired
    private PersonService personService;
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @Transactional
    public void contextLoads() {
        Person person = personService.createPerson(1, "person1");
        Assert.assertNotNull(person);

        ResponseEntity<Person[]> persons = restTemplate.getForEntity("/persons", Person[].class);
    }
}

The test does not work, because the service is not saving the Person entity .... Thanks in advance

Laur Ivan
  • 4,117
  • 3
  • 38
  • 62
Hedi Ayed
  • 371
  • 2
  • 3
  • 12
  • 18
    Well of course not... Your test is transactional so the transaction wraps your test method. So it is rollback afterwards (and your service participates in that transaction). Even if you would `flush` it wouldn't work as you are using rest to access the application which would open a new transaction. The only way to make this work is to make the test not transactional. – M. Deinum Apr 27 '17 at 13:02
  • You can try split your test. Create private method saving the person with transactional annotated and call it from the test method. – kimy82 Apr 27 '17 at 13:28
  • As @M.Deinum said, the transaction rolls back in your test and you see no changes. Try adding '@Commit' or @Rollback(false) to your test. – dev4Fun Apr 27 '17 at 13:49
  • 3
    That won't work either, as there are 2 transactions involved and as the first one hasn't committed yet before he is calling the rest service. The latter will start another transaction but as the other is still not committed it cannot see that data (remember the I in ACID for Isolation) – M. Deinum Apr 27 '17 at 14:12

6 Answers6

20
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class SmokeTest {

    @Autowired
    UserController userController;

    @Autowired
    UserDao userDAO;

    @Rollback(false) // This is key to avoid rollback.
    @Test   
    public void contextLoads() throws Exception {
        System.out.println("Hiren");

        System.out.println("started");
        userDAO.save(new User("tyx", "x@x.com"));
    }
}

Refer @Rollback(false) is key to avoid rollback.

ansidev
  • 361
  • 1
  • 3
  • 17
user2758406
  • 536
  • 7
  • 16
  • 1
    since spring 4.2 wa can use @Commit instead – Joand Jan 26 '22 at 16:07
  • Surprisingly the `@Rollback(false)` did not work in spring boot 3 while `@Commit` does the job – Alireza Fattahi Jan 24 '23 at 13:59
  • It helps when you use method with @Transactional(propagation = Propagation.REQUIRES_NEW) in catch block. First transaction that came from test context is marked as rollback and you are starting new transaction that makes deadlock in the test context. To avoid that I'm using TestTransaction.start() and TestTransaction.end(); but @Rollback(false) was missing. – Falcon Aug 17 '23 at 09:28
6

Thanks to M. Deinum, I think I get the point, So the best is to separate the logic of the test into two tests, the first will testing just the service (so this one could be transactional) and the second the controller:

Test 1:

@Test
@Transactional
public void testServiceSaveAndRead() {
    personService.createPerson(1, "person1");
    Assert.assertTrue(personService.getPersons().size() == 1);
}

Test 2:

@MockBean
private PersonService personService;

@Before
public void setUp() {
    //mock the service
    given(personService.getPersons())
            .willReturn(Collections.singletonList(new Person(1, "p1")));
}

@Test
public void testController() {
    ResponseEntity<Person[]> persons = restTemplate.getForEntity("/persons", Person[].class);
    Assert.assertTrue(persons.getBody()!=null && persons.getBody().length == 1);
}
Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
Hedi Ayed
  • 371
  • 2
  • 3
  • 12
5

Spring for saving entity requires transaction. But until transaction has been commited changes not be visible from another transaction.

Simplest way is call controller after commit transaction

@Test
@Transactional
public void contextLoads() {
    Person person = personService.createPerson(1, "person1");
    Assert.assertNotNull(person);

    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            ResponseEntity<Person[]> persons = restTemplate.getForEntity("/persons", Person[].class);
        }
    });        
}
Nick
  • 3,691
  • 18
  • 36
  • Absolutely right! The test should not permanently alter the DB, as other answers suggest. – Michael Piefel Jan 31 '21 at 09:13
  • I think it is important to note that using this method, I was not able to debug the code within the `afterCommit` method. I was using Inellij, and the debugger wouldn't go in there. So if you need to have the ability to debug your tests, this won't work. – JCB Dec 17 '21 at 19:17
  • @JCB afterCommit work only inside transaction – Nick Dec 18 '21 at 09:46
1

For each @Test function that makes a DB transaction, if you want to permanently persist the changes, then you can use @Rollback(false)

@Rollback(false)
@Test
public void createPerson() throws Exception {
    int databaseSizeBeforeCreate = personRepository.findAll().size();

    // Create the Person
    restPersonMockMvc.perform(post("/api/people")
        .contentType(TestUtil.APPLICATION_JSON_UTF8)
        .content(TestUtil.convertObjectToJsonBytes(person)))
        .andExpect(status().isCreated());

    // Validate the Person in the database
    List<Person> personList = personRepository.findAll();
    assertThat(personList).hasSize(databaseSizeBeforeCreate + 1);
    Person testPerson = personList.get(personList.size() - 1);
    assertThat(testPerson.getFirstName()).isEqualTo(DEFAULT_FIRST_NAME);
    assertThat(testPerson.getLastName()).isEqualTo(DEFAULT_LAST_NAME);
    assertThat(testPerson.getAge()).isEqualTo(DEFAULT_AGE);
    assertThat(testPerson.getCity()).isEqualTo(DEFAULT_CITY);
}

I tested it with a SpringBoot project generated by jHipster:

  • SpringBoot: 1.5.4
  • jUnit 4.12
  • Spring 4.3.9
1

Do not use @Rollback(false). Unit Test should not generate data.

JPA FlushMode is AUTO (default - flush INSERT/UPDATE/DELETE SQL when query occurs) / COMMIT.

Just query the working entity for forcing FLUSH, or using EntityManager to force flush

@Test
public void testCreate(){
    InvoiceRange range = service.createInvoiceRange(1, InvoiceRangeCreate.builder()
            .form("01GTKT0/010")
            .serial("NV/18E")
            .effectiveDate(LocalDate.now())
            .rangeFrom(1L)
            .rangeTo(1000L)
            .build(), new byte[] {1,2,3,4,5});

    service.findByCriteria(1, "01GTKT0/010", "NV/18E");  // force flush
    // em.flush(); // another way is using entityManager for force flush
}
Neo Pham
  • 362
  • 3
  • 9
1

Pay your attention to the order in which the tests are executed, the tests with the @Commit or @Rollback(false) annotation must be executed first: https://www.baeldung.com/junit-5-test-order

Denis
  • 11
  • 1