How does @Transactional work on test methods?
Asked Answered
G

1

6

I'm wondering how @Transactional works with Spring Data JPA / Hibernate on test methods. I searched some explanations on the web but it still seems obscure.

Below is the code I'm using:

Member.java

@Entity 
@Table(name = "MEMBER")
public class Member  {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    String firstname;

    String lastname;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
          name = "MEMBER_ROLE",
          joinColumns = @JoinColumn(name = "member_id"),
          inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<Role>();

    // Getters...
    // Setters...

}

Role.java

@Entity
@Table(name = "ROLE")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String description;

    public Role(String name, String description) {

        this.name = name;
        this.description = description;
    }

    @ManyToMany(mappedBy = "roles", cascade = CascadeType.ALL)
    private Set<User> users = new HashSet<User>();

    public void addUser(User user) {
        users.add(user);
        user.getRoles().add(this);
    }

    // Getters...
    // Setters...

}

MemberRepositoryTest.java

@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;


    @Test
    @Transactional
    //@Commit // When @Commit is uncommented, the relation is saved in the database
    public void testSave() {

        Member testMember = new Member();

        testMember.setFirstname("John");
        testMember.setLastname("Doe");

        Role role1 = new Role("ROLE_USER", "The role of for basic users");
        Role role2 = new Role("ROLE_ADMIN", "The role of for admin users");

        testMember.getRoles().add(role1);
        testMember.getRoles().add(role2);

        this.memberRepository.save(testMember);

        // The 2 roles are well in the set
        System.out.println("********" + this.memberRepository.findById(1).get().getRoles().toString());

    }

}

  • The thing is that when I use @Transactional alone without @Commit, only the user and the role entries are created. The table MEMBER_ROLE is created but empty (no entry): the n:n @ManyToMany unidirectional relation is not saved.
  • When using @Commit, everything works properly: there are 2 entries corresponding to the role of the John Doe user.

Here are my questions:

How does CrudRepository.save(...) work under the hood with @Transactional and why using this annotation alone does a "partial save"? We should expect that without @Commit nothing is committed to the database (and everything is roll backed).

Is it a good practice to use @Transactional on test methods?

Gunflint answered 2/6, 2020 at 17:27 Comment(1)
I've never use Transaction on the test method, all the time, I use it in Service class Test class in the normal way do init the data, perform test, and rollback the data change to give the original of testing dataset, that ensures no data is modified after every single test case run. To make the next test-case does not get effected from the previous one and the test will run with the same result everytime.Speakeasy
W
10

How does CrudRepository.save(...) work under the hood with @Transactional and why using this annotation alone does a "partial save"? We should expect that without @Commit nothing is committed to the database (and everything is roll backed).

Since it's a test, the transaction should be by default rolledback thanks to TransactionalTestExecutionListener. Now you are asking why something is committed anyway, it can be because of inner transactions. @Transactional spans the transaction for entire test method, so if you use some dao (like in your case) that transaction will be rolledback also, however if some method uses transaction propagation type other than REQUIRED, for example REQUIRED_NEW, call to db can be performed anyway, because REQUIRED_NEW suspends current transaction from the test and creates a new one. That new one will be committed. There is also flush time issue, due hibernate caching - he makes a call to db at the end of transaction, usually at the end of the method, so fetching elements directly after persisting them may fail. Both things can be tricky.

Is it a good practice to use @Transactional on test methods?

Sure, but not to test transaction boundaries. Remember that you can use also @DataJpaTest, which places @Transactional for every test method.

Sources:

  1. TransactionalTestExecutionListener
  2. Automatic Rollback of Transactions in Spring Tests
Warram answered 17/8, 2020 at 21:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.