Reputation: 1691
My Spring Boot app has the following classes:
Board (JPA entity)
@Entity
@Table(name = "board")
public class Board {
public static final int IN_PROGRESS = 1;
public static final int AFK = 2;
public static final int COMPLETED = 3;
@Column(name = "id")
@Generated(GenerationTime.INSERT)
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@Column(name = "status", nullable = false)
private int status = IN_PROGRESS;
}
BoardRepository (JPA repository)
public interface BoardRepository extends JpaRepository<Board, Long> {}
CommonBoardService (base service)
public interface CommonBoardService {
Board save(Board board);
Board update(Board board, int status);
}
CommonBoardServiceImpl (base service implementation)
@Service
@Transactional
public class CommonBoardServiceImpl implements CommonBoardService {
@Autowired
private BoardRepository boardRepository;
public Board save(final Board board) {
return boardRepository.save(board);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Board update(final Board board, final int status) {
board.setStatus(status);
return save(board);
}
}
BoardService (specific service interface)
public interface BoardService {
Board startBoard();
void synchronizeBoardState(Board board);
}
BoardServiceImpl (specific service implementation)
@Service
@Transactional
public class BoardServiceImpl implements BoardService {
@Autowired
private CommonBoardService commonBoardService;
public Board startBoard() { return new Board(); }
public void synchronizeBoardState(final Board board) {
if (board != null && inProgress(board)) {
if (!canPlayWithCurrentBoard(board)) {
commonBoardService.update(board, Board.AFK);
}
else {
commonBoardService.update(board, Board.COMPLETED);
}
}
}
private boolean canPlayWithCurrentBoard(final Board board) {
return !inProgress(board);
}
private boolean inProgress(final Board board) {
return board != null && board.getStatus() == Board.IN_PROGRESS;
}
}
BoardServiceTest (unit test)
1. @RunWith(SpringJUnit4ClassRunner.class)
2. @Transactional
3. public class BoardServiceTest {
4. @Autowired
5. private BoardRepository boardRepository;
6.
7. @Autowired
8. private BoardService boardService;
9. @Autowired
10. private CommonBoardService commonBoardService;
11.
12. @Test
13. public void testSynchronizeBoardStatus() {
14. Board board = boardService.startBoard();
15.
16. board = commonBoardService.save(board);
17.
18. assertEquals(1, boardRepository.count());
19.
20. boardService.synchronizeBoardState(board);
21.
22. assertEquals(1, boardRepository.count());
23. }
24. }
This test fails on line 22 with the error java.lang.AssertionError: Expected :1 Actual:2
. Hibernate SQL logs reveal an INSERT
being fired on line 20 instead of an UPDATE
. Since I am using the same Board
object throughout the test, I expect line 20 to fire an UPDATE
instead of an INSERT
.
Can anyone explain why this is happening and how to get the expected behaviour (UPDATE
on line 20)?
Upvotes: 0
Views: 621
Reputation: 20135
The culprit is this line: @Transactional(propagation = Propagation.REQUIRES_NEW)
. Lets see what happens when the test case is executed.
BoardServiceTest
is annotated with @Transactional
a new transaction is started when BoardServiceTest.testSynchronizeBoardStatus
starts executing.Board
instance.Board
instance created on line 14 and triggers a database INSERT
.CommonBoardServiceImpl.update
which is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW)
. This suspends the ongoing transaction (see the JavaDocs for Propagation), which has neither been committed nor been rolled back so far.CommonBoardServiceImpl.update
in turn attempts to save the Board
instance passed to it.INSERT
.CommonBoardServiceImpl.update
. The outer transaction resumes.SELECT
query. This means there are now two instances in the database, hence the test failure.Removing @Transactional(propagation = Propagation.REQUIRES_NEW)
ensures that the entire test is executed within the same transaction and hence passes.
Upvotes: 1