Skip to main content

Testing Guide

Comprehensive guide for testing applications using Spring Boot Result Starter.

Unit Testing Results

Basic Result Testing

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

@Test
void findById_WhenUserExists_ReturnsSuccess() {
// Given
Long userId = 1L;
User user = new User("John Doe", "john@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));

// When
Result<User> result = userService.findById(userId);

// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData()).isEqualTo(user);
assertThat(result.getError()).isNull();
}

@Test
void findById_WhenUserNotExists_ReturnsNotFoundError() {
// Given
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());

// When
Result<User> result = userService.findById(userId);

// Then
assertThat(result.isSuccess()).isFalse();
assertThat(result.getData()).isNull();
assertThat(result.getError()).isInstanceOf(EntityNotFoundError.class);
assertThat(result.getError().getMessage()).contains("User not found");
}
}

Testing Validation Chains

@Test
void createUser_WithInvalidData_ReturnsValidationError() {
// Given
CreateUserRequest request = new CreateUserRequest("", "invalid-email");

// When
Result<User> result = userService.createUser(request);

// Then
assertThat(result.isSuccess()).isFalse();
assertThat(result.getError()).isInstanceOf(ValidationError.class);
// Test stops at first validation failure
assertThat(result.getError().getMessage()).isEqualTo("Name is required");
}

@Test
void createUser_WithValidData_ReturnsSuccess() {
// Given
CreateUserRequest request = new CreateUserRequest("John Doe", "john@example.com");
User savedUser = new User("John Doe", "john@example.com");

when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);

// When
Result<User> result = userService.createUser(request);

// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData().getName()).isEqualTo("John Doe");
assertThat(result.getData().getEmail()).isEqualTo("john@example.com");
}

Integration Testing

Testing Controllers with Results

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private UserRepository userRepository;

@Test
void getUser_WhenUserExists_ReturnsSuccessResponse() {
// Given
User user = userRepository.save(new User("John Doe", "john@example.com"));

// When
ResponseEntity<ResponseWrapper> response = restTemplate.getForEntity(
"/api/users/" + user.getId(),
ResponseWrapper.class
);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().isSuccess()).isTrue();
assertThat(response.getBody().getMessage()).contains("successfully");
assertThat(response.getBody().getData()).isNotNull();
}

@Test
void getUser_WhenUserNotExists_ReturnsNotFoundResponse() {
// When
ResponseEntity<ResponseWrapper> response = restTemplate.getForEntity(
"/api/users/999",
ResponseWrapper.class
);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(response.getBody().isSuccess()).isFalse();
assertThat(response.getBody().getMessage()).contains("not found");
assertThat(response.getBody().getData()).isNull();
}
}

Testing Async Operations

@Test
void getUserAsync_ReturnsCompletedFuture() throws Exception {
// Given
User user = new User("John Doe", "john@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// When
CompletableFuture<Result<User>> futureResult = Result.async(() -> userService.findById(1L));

// Then
Result<User> result = futureResult.get(5, TimeUnit.SECONDS);
assertThat(result.isSuccess()).isTrue();
assertThat(result.getData()).isEqualTo(user);
}

Testing with MockMvc

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void getUser_WhenUserExists_ReturnsOk() throws Exception {
// Given
User user = new User("John Doe", "john@example.com");
when(userService.findById(1L)).thenReturn(Result.success(user));

// When & Then
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.name").value("John Doe"))
.andExpect(jsonPath("$.data.email").value("john@example.com"));
}

@Test
void getUser_WhenUserNotExists_ReturnsNotFound() throws Exception {
// Given
when(userService.findById(999L)).thenReturn(Result.entityNotFoundError("User not found"));

// When & Then
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value("User not found"))
.andExpect(jsonPath("$.data").isEmpty());
}
}

Testing Event Publishing

@SpringBootTest
class EventPublishingTest {

@Autowired
private UserService userService;

@Autowired
private ApplicationEventPublisher eventPublisher;

@MockBean
private UserRepository userRepository;

@EventListener
@Component
static class TestEventListener {
private final List<ResultEvent<?>> receivedEvents = new ArrayList<>();

@EventListener
public void handleResultEvent(ResultEvent<?> event) {
receivedEvents.add(event);
}

public List<ResultEvent<?>> getReceivedEvents() {
return receivedEvents;
}

public void clear() {
receivedEvents.clear();
}
}

@Autowired
private TestEventListener eventListener;

@Test
void createUser_OnSuccess_PublishesEvent() {
// Given
CreateUserRequest request = new CreateUserRequest("John Doe", "john@example.com");
User savedUser = new User("John Doe", "john@example.com");

when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);

eventListener.clear();

// When
Result<User> result = userService.createUser(request);

// Then
assertThat(result.isSuccess()).isTrue();
assertThat(eventListener.getReceivedEvents()).hasSize(1);

ResultEvent<?> event = eventListener.getReceivedEvents().get(0);
assertThat(event.getEventName()).isEqualTo("user-created");
assertThat(event.isSuccess()).isTrue();
}
}

Testing Transaction Rollback

@SpringBootTest
@Transactional
class TransactionRollbackTest {

@Autowired
private UserService userService;

@Autowired
private UserRepository userRepository;

@Test
void createUser_WhenValidationFails_RollsBackTransaction() {
// Given
CreateUserRequest request = new CreateUserRequest("", "john@example.com");
long initialCount = userRepository.count();

// When
Result<User> result = userService.createUser(request);

// Then
assertThat(result.isSuccess()).isFalse();
assertThat(userRepository.count()).isEqualTo(initialCount); // No new user saved
}
}

Custom Test Utilities

Result Matchers

public class ResultMatchers {

public static <T> Matcher<Result<T>> isSuccess() {
return new TypeSafeMatcher<Result<T>>() {
@Override
protected boolean matchesSafely(Result<T> result) {
return result.isSuccess();
}

@Override
public void describeTo(Description description) {
description.appendText("a successful Result");
}
};
}

public static <T> Matcher<Result<T>> isFailure() {
return new TypeSafeMatcher<Result<T>>() {
@Override
protected boolean matchesSafely(Result<T> result) {
return !result.isSuccess();
}

@Override
public void describeTo(Description description) {
description.appendText("a failed Result");
}
};
}

public static <T> Matcher<Result<T>> hasErrorType(Class<? extends Error> errorType) {
return new TypeSafeMatcher<Result<T>>() {
@Override
protected boolean matchesSafely(Result<T> result) {
return !result.isSuccess() && errorType.isInstance(result.getError());
}

@Override
public void describeTo(Description description) {
description.appendText("a Result with error type " + errorType.getSimpleName());
}
};
}
}

Usage with Custom Matchers

@Test
void findById_WhenUserNotExists_ReturnsEntityNotFoundError() {
// Given
when(userRepository.findById(999L)).thenReturn(Optional.empty());

// When
Result<User> result = userService.findById(999L);

// Then
assertThat(result, isFailure());
assertThat(result, hasErrorType(EntityNotFoundError.class));
}

Performance Testing

@Test
void resultPattern_PerformanceComparison() {
int iterations = 100_000;

// Test Result pattern performance
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
Result<String> result = Result.entityNotFoundError("Not found");
if (!result.isSuccess()) {
// Handle error
}
}
long resultTime = System.nanoTime() - startTime;

// Test exception performance
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
try {
throw new RuntimeException("Not found");
} catch (RuntimeException e) {
// Handle error
}
}
long exceptionTime = System.nanoTime() - startTime;

System.out.println("Result pattern: " + resultTime / 1_000_000 + "ms");
System.out.println("Exception pattern: " + exceptionTime / 1_000_000 + "ms");

// Result pattern should be significantly faster
assertThat(resultTime).isLessThan(exceptionTime / 2);
}

Best Practices for Testing

1. Test Both Success and Failure Cases

Always test both the happy path and error scenarios.

2. Use Descriptive Test Names

// Good
void createUser_WhenEmailAlreadyExists_ReturnsEntityAlreadyExistsError()

// Bad
void testCreateUser()

3. Test Validation Chains Thoroughly

@ParameterizedTest
@ValueSource(strings = {"", " ", "a", "very-long-name-that-exceeds-fifty-characters-limit"})
void createUser_WithInvalidName_ReturnsValidationError(String invalidName) {
CreateUserRequest request = new CreateUserRequest(invalidName, "john@example.com");
Result<User> result = userService.createUser(request);
assertThat(result, isFailure());
assertThat(result, hasErrorType(ValidationError.class));
}

4. Test Async Operations with Timeouts

@Test
@Timeout(5)
void asyncOperation_CompletesWithinTimeout() throws Exception {
CompletableFuture<Result<User>> future = userService.createUserAsync(request);
Result<User> result = future.get();
assertThat(result, isSuccess());
}