Reputation: 11267
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
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