Testing a @Transactional Spring Service
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
Testing a Transactional service depends on what you want to verify.
For unit tests, you ignore the at-Transactional annotation completely. You mock the repository using at-Mock, stub its methods with Mockito, and test your business logic—like does the service validate the order correctly, does it call save in the right sequence. The at-Transactional annotation doesn't do anything in this context because there's no Spring context to create the transaction proxy.
It's just a decoration on the method. For integration tests, you actually need Spring to be running. You'd use at-DataJpaTest if you're testing repository interactions, or at-SpringBootTest if you need the full application context.
This loads Spring's transaction infrastructure, so now at-Transactional actually works. You can verify things like: if the method throws an exception, does the transaction roll back so no data is saved to the database. Or if you save multiple entities, do they all commit together in one transaction.
The key thing freshers miss is that at-Transactional is a Spring AOP proxy feature—it wraps your method at runtime with transaction management code. Without Spring running, that proxy doesn't exist, so the annotation has no effect. So my approach is: unit tests for business logic with mocks, integration tests for transaction behavior with a real database.
Testing a @Transactional Spring Service
Direct Answer
For unit tests, mock the repository and test business logic without transactions—@Transactional has no effect without a Spring context. For integration tests, use @DataJpaTest or @SpringBootTest to load the transaction infrastructure, then verify transaction behavior like rollback on exceptions or cascade operations.
Code Example
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private InventoryService inventoryService;
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService(orderRepository, inventoryService);
}
@Test
void placeOrder_savesOrderAndReducesInventory() {
var order = new Order(1L, "Laptop", 2);
when(inventoryService.reserveStock("Laptop", 2)).thenReturn(true);
when(orderRepository.save(order)).thenReturn(order);
var result = orderService.placeOrder(order); // @Transactional ignored here
verify(inventoryService).reserveStock("Laptop", 2);
verify(orderRepository).save(order); // verify business logic flow
assertEquals(order, result);
}
}
Common Mistakes & Pitfalls
- Testing transactions without Spring: a plain Mockito test cannot exercise Spring's transactional proxy.
- Assuming every exception rolls back: by default, Spring rolls back for unchecked exceptions; checked-exception behavior needs explicit configuration.
- Ignoring self-invocation: a call from one method to another on the same object can bypass proxy interception.
- Confusing test rollback with service rollback: a test-managed transaction can hide what would commit in production.
- Using a broad context for plain validation: keep business-rule unit tests separate from transaction integration tests.
Check your understanding
Answer these without looking back at the article.
- What is the difference between @DataJpaTest and @SpringBootTest for repository testing?
- Why does @Transactional on a test class cause automatic rollback?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.