mikeb
mikeb

Reputation: 11267

Spring Boot JPA Transaction during Test - not throwing key violation exception on insert

I have the following test that generates an invitation with a company id and an email. Then, we generate a second with the same company id and email. This should violate the unique constraint on the DB (See the Invitation entity). Thing is, the test passes as listed below (the transaction is never committed for the second insert, I guess)

If I do a findAll() after the second insert (see the comment below) I get an exception on the line where the findAll() is that the index violation has occurred.

How do I make the methods in the UserService commit their transactions when the method returns, so the test code will throw an exception as-is?

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserServiceTest {
    ... Everything is autowired
    @Test
    public void testCreateInvitation() {
        String email = "[email protected]";
        Company company = userService.createCompany("ACMETEST", tu);

        assertTrue(invitationRepository.count() == 0l);
        assertNotNull(userService.sendInvitation(company, email));
        assertTrue(invitationRepository.count() == 1l);

        assertTrue(invitationRepository.findAllByEmail(email).size() == 1);
        // Second should throw exception
        userService.sendInvitation(company, email);

        // If this line is uncommented, I get key violation exception, otherwise, test passes!!!!????
        //Iterable<Invitation> all = invitationRepository.findAll();
    }

Invitation class and constraint:

@Entity
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"email", "company_id"}))
@JsonIdentityInfo(generator= ObjectIdGenerators.PropertyGenerator.class, property="id")
@Data
public class Invitation {

    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy = "uuid")
    String id;

    @ManyToOne
    @JoinColumn(name="company_id")
    private Company company;

    String email;

    Date inviteDate;

    Date registerDate;
}

User Service:

@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class UserService {
    public Invitation sendInvitation(Company company, String toEmail) {
        Invitation invitation = new Invitation();
        invitation.setCompany(company);
        invitation.setInviteDate(new Date());
        invitation.setEmail(toEmail);
        try {
            // TODO send email
            return invitationRepository.save(invitation);
        } catch (Exception e) {
            LOGGER.error("Cannot send invitation: {}", e.getMessage());
        }
        return null;
    }
}

Upvotes: 4

Views: 2106

Answers (1)

Denis Zavedeev
Denis Zavedeev

Reputation: 8297

How do I make the methods in the UserService commit their transactions when the method returns, so the test code will throw an exception as-is?

Since @DataJpaTest is @Transactional itself. Code written in @Test method is executed within a transaction. You can change it using Propogation.NOT_SUPPORTED which

Execute non-transactionally, suspend the current transaction if one exists

@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testCreateInvitation() {
    // your test method body unchanged
}

Using this you should get an exception on the second userService.sendInvitation(company, email); call.

Also there are some other options.

Option 1:

Add @Commit to your test method.

All tests marked with @Transactional are rolled back after its execution. But there is a subtle thing with Hibernate: it may delay executing sql until you commit a transaction (or call flush explicitly (option 2)).

Since a transaction rollbacked no insertions performed hence there is no constraint violation.

Beware that using @Commit you will get an exception thrown outside of your test method.

Option 2:

Use manual flush():

If your repository extends JpaRepository<>:

@Test
public void testCreateInvitation() {
    // create first invitation,
    // create second invitation
    invitationRepository.flush();
}

Else inject EntityManager and call flush() on it:

@PersistenceContext
private EntityManager em;

@Test
public void testCreateInvitation() {
    // create first invitation,
    // create second invitation
    em.flush();
}

You will get an exception when invitationRepository.flush() or em.flush() called.

Option 3: Use GenerationType.IDENTITY:

@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;

Hibernate must assign id to an @Entity after you save() it. But it has no way of getting an id for an entity without executing an insert.

P.S. There is a one thing confusing me.

You use @Transactional(propagation = Propagation.REQUIRES_NEW) and it should force commiting a transaction after the method is executed. Please check your logs to see if transactions are actually working.

Upvotes: 5

Related Questions