Comprehensive application framework and inversion of control container for the Java platform providing dependency injection, AOP, data access, transaction management, and web framework capabilities
Spring Test provides comprehensive testing support for Spring applications, including the TestContext Framework, MockMvc for web layer testing, transaction rollback support, and integration with popular testing frameworks like JUnit and TestNG.
<!-- Spring Test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.39</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>
<!-- AssertJ -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test (if using Spring Boot) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>// TestContext Framework
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.annotation.DirtiesContext;
// Spring Boot Testing
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
// MockMvc
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
// WebTestClient (WebFlux)
import org.springframework.test.web.reactive.server.WebTestClient;
// Transaction Testing
import org.springframework.test.annotation.Rollback;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
// JUnit 5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
// Assertions
import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;// Combination of @ExtendWith(SpringExtension.class) and @ContextConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public @interface SpringJUnitConfig {
@AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
Class<?>[] value() default {};
@AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
Class<?>[] classes() default {};
@AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
String[] locations() default {};
}
// Defines class-level metadata for configuring TestContext
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ContextConfiguration {
@AliasFor("locations")
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
boolean inheritLocations() default true;
boolean inheritInitializers() default true;
String name() default "";
}
// Indicates which active bean definition profiles should be used
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ActiveProfiles {
@AliasFor("profiles")
String[] value() default {};
String[] profiles() default {};
boolean inheritProfiles() default true;
Class<? extends ActiveProfilesResolver> resolver() default ActiveProfilesResolver.class;
}
// Configures test property sources
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(TestPropertySources.class)
public @interface TestPropertySource {
@AliasFor("locations")
String[] value() default {};
String[] locations() default {};
boolean inheritLocations() default true;
String[] properties() default {};
boolean inheritProperties() default true;
}
// Indicates that ApplicationContext should be closed and removed from cache
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DirtiesContext {
ScopeMode value() default ScopeMode.DEFAULT_SCOPE;
ClassMode classMode() default ClassMode.AFTER_CLASS;
MethodMode methodMode() default MethodMode.AFTER_METHOD;
Class<? extends HierarchyMode> hierarchyMode() default HierarchyMode.EXHAUSTIVE;
enum ClassMode {
BEFORE_CLASS, BEFORE_EACH_TEST_METHOD, AFTER_EACH_TEST_METHOD, AFTER_CLASS
}
enum MethodMode {
BEFORE_METHOD, AFTER_METHOD
}
enum HierarchyMode {
CURRENT_LEVEL, EXHAUSTIVE
}
}// Encapsulates the context for running a test
public interface TestContext extends AttributeAccessor, Serializable {
ApplicationContext getApplicationContext();
Class<?> getTestClass();
Object getTestInstance();
Method getTestMethod();
Throwable getTestException();
void markApplicationContextDirty(DirtiesContext.HierarchyMode hierarchyMode);
void updateState(Object testInstance, Method testMethod, Throwable testException);
}
// Defines a strategy API for providing ApplicationContext instances
public interface ContextLoader {
String[] processLocations(Class<?> clazz, String... locations);
ApplicationContext loadContext(String... locations) throws Exception;
}
// Strategy for loading ApplicationContext from annotated classes
public class AnnotationConfigContextLoader extends AbstractGenericContextLoader {
@Override
protected BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context);
@Override
protected String getResourceSuffix();
}// Annotation for integration tests
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
@AliasFor("classes")
Class<?>[] value() default {};
Class<?>[] classes() default {};
String[] properties() default {};
WebEnvironment webEnvironment() default WebEnvironment.MOCK;
// WebEnvironment enum
enum WebEnvironment {
MOCK, // Load WebApplicationContext with mock web environment
RANDOM_PORT, // Load WebServerApplicationContext with random port
DEFINED_PORT, // Load WebServerApplicationContext with defined port
NONE // Load ApplicationContext without web environment
}
}
// Annotation for testing web layer
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
@AliasFor("controllers")
Class<?>[] value() default {};
Class<?>[] controllers() default {};
boolean useDefaultFilters() default true;
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
String[] properties() default {};
}
// Annotation for testing JPA repositories
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
boolean showSql() default true;
boolean useDefaultFilters() default true;
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
String[] properties() default {};
}// Annotation to add mock objects to Spring ApplicationContext
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockBeans.class)
public @interface MockBean {
String name() default "";
Class<?>[] value() default {};
Class<?>[] classes() default {};
Class<?>[] extraInterfaces() default {};
Answers answer() default Answers.RETURNS_DEFAULTS;
boolean serializable() default false;
MockReset reset() default MockReset.AFTER;
}
// Annotation to add spy objects to Spring ApplicationContext
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(SpyBeans.class)
public @interface SpyBean {
String name() default "";
Class<?>[] value() default {};
Class<?>[] classes() default {};
MockReset reset() default MockReset.AFTER;
}
// MockReset enum for controlling mock reset behavior
public enum MockReset {
BEFORE, AFTER, NONE
}// Main entry point for server-side Spring MVC testing
public final class MockMvc {
// Perform a request
public ResultActions perform(RequestBuilder requestBuilder) throws Exception;
// Static factory methods
public static MockMvcBuilder standaloneSetup(Object... controllers);
public static DefaultMockMvcBuilder webAppContextSetup(WebApplicationContext context);
}
// Builder for creating MockMvc instances
public interface MockMvcBuilder {
MockMvc build();
}
// Default MockMvc builder
public class DefaultMockMvcBuilder extends AbstractMockMvcBuilder<DefaultMockMvcBuilder> {
public DefaultMockMvcBuilder addFilters(Filter... filters);
public DefaultMockMvcBuilder addFilter(Filter filter, String... urlPatterns);
public DefaultMockMvcBuilder dispatchOptions(boolean dispatchOptions);
public DefaultMockMvcBuilder defaultRequest(RequestBuilder requestBuilder);
public DefaultMockMvcBuilder alwaysExpect(ResultMatcher resultMatcher);
public DefaultMockMvcBuilder alwaysDo(ResultHandler resultHandler);
}
// Interface for building requests
public interface RequestBuilder {
MockHttpServletRequest buildRequest(ServletContext servletContext);
}
// Interface for actions that can be performed on the result
public interface ResultActions {
ResultActions andExpect(ResultMatcher matcher) throws Exception;
ResultActions andDo(ResultHandler handler) throws Exception;
MvcResult andReturn();
}
// Interface for asserting the result
@FunctionalInterface
public interface ResultMatcher {
void match(MvcResult result) throws Exception;
}
// Interface for handling the result
@FunctionalInterface
public interface ResultHandler {
void handle(MvcResult result) throws Exception;
}// Utility class for building MockMvc requests
public class MockMvcRequestBuilders {
// HTTP method builders
public static MockHttpServletRequestBuilder get(String urlTemplate, Object... uriVars);
public static MockHttpServletRequestBuilder get(URI uri);
public static MockHttpServletRequestBuilder post(String urlTemplate, Object... uriVars);
public static MockHttpServletRequestBuilder post(URI uri);
public static MockHttpServletRequestBuilder put(String urlTemplate, Object... uriVars);
public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... uriVars);
public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... uriVars);
// File upload
public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... uriVars);
public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... uriVars);
// Async
public static RequestBuilder asyncDispatch(MvcResult mvcResult);
}
// Builder for HTTP servlet requests
public class MockHttpServletRequestBuilder implements ConfigurableSmartRequestBuilder<MockHttpServletRequestBuilder> {
// Request line
public MockHttpServletRequestBuilder contextPath(String contextPath);
public MockHttpServletRequestBuilder servletPath(String servletPath);
public MockHttpServletRequestBuilder pathInfo(String pathInfo);
// Parameters
public MockHttpServletRequestBuilder param(String name, String... values);
public MockHttpServletRequestBuilder params(MultiValueMap<String, String> params);
public MockHttpServletRequestBuilder queryParam(String name, String... values);
public MockHttpServletRequestBuilder queryParams(MultiValueMap<String, String> params);
// Headers
public MockHttpServletRequestBuilder header(String name, Object... values);
public MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders);
public MockHttpServletRequestBuilder accept(MediaType... mediaTypes);
public MockHttpServletRequestBuilder contentType(MediaType mediaType);
// Body
public MockHttpServletRequestBuilder content(String content);
public MockHttpServletRequestBuilder content(byte[] content);
public MockHttpServletRequestBuilder characterEncoding(String encoding);
// Session and security
public MockHttpServletRequestBuilder session(MockHttpSession session);
public MockHttpServletRequestBuilder sessionAttr(String name, Object value);
public MockHttpServletRequestBuilder flashAttr(String name, Object value);
public MockHttpServletRequestBuilder cookie(Cookie... cookies);
public MockHttpServletRequestBuilder locale(Locale locale);
public MockHttpServletRequestBuilder principal(Principal principal);
// Request attributes
public MockHttpServletRequestBuilder requestAttr(String name, Object value);
public MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor);
}// Utility class for asserting MockMvc results
public class MockMvcResultMatchers {
// Status
public static StatusResultMatchers status();
// Headers
public static HeaderResultMatchers header();
// Content
public static ContentResultMatchers content();
// JSON
public static JsonPathResultMatchers jsonPath(String expression, Object... args);
public static JsonPathResultMatchers jsonPath(String expression, Matcher<T> matcher);
// View and Model
public static ViewResultMatchers view();
public static ModelResultMatchers model();
// Flash attributes
public static FlashAttributeResultMatchers flash();
// Redirects
public static ResultMatcher redirectedUrl(String expectedUrl);
public static ResultMatcher redirectedUrlPattern(String redirectedUrlPattern);
public static ResultMatcher forwardedUrl(String expectedUrl);
// Cookies
public static CookieResultMatchers cookie();
// XPath
public static XpathResultMatchers xpath(String expression, Object... args);
}
// Status result matchers
public class StatusResultMatchers {
public ResultMatcher is(int status);
public ResultMatcher isOk();
public ResultMatcher isCreated();
public ResultMatcher isAccepted();
public ResultMatcher isNoContent();
public ResultMatcher isBadRequest();
public ResultMatcher isUnauthorized();
public ResultMatcher isForbidden();
public ResultMatcher isNotFound();
public ResultMatcher isMethodNotAllowed();
public ResultMatcher isConflict();
public ResultMatcher isInternalServerError();
}
// Content result matchers
public class ContentResultMatchers {
public ResultMatcher contentType(String contentType);
public ResultMatcher contentType(MediaType contentType);
public ResultMatcher contentTypeCompatibleWith(MediaType contentType);
public ResultMatcher string(String expectedContent);
public ResultMatcher string(Matcher<? super String> matcher);
public ResultMatcher bytes(byte[] expectedContent);
public ResultMatcher xml(String xmlContent);
public ResultMatcher json(String jsonContent);
public ResultMatcher json(String jsonContent, boolean strict);
}// Non-blocking, reactive client for testing web servers
public interface WebTestClient {
// Request specification
RequestHeadersUriSpec<?> get();
RequestBodyUriSpec post();
RequestBodyUriSpec put();
RequestBodyUriSpec patch();
RequestHeadersUriSpec<?> delete();
RequestHeadersUriSpec<?> options();
RequestHeadersUriSpec<?> head();
RequestBodyUriSpec method(HttpMethod method);
// Static factory methods
static WebTestClient bindToServer();
static WebTestClient bindToWebHandler(WebHandler webHandler);
static WebTestClient bindToRouterFunction(RouterFunction<?> routerFunction);
static WebTestClient bindToController(Object... controllers);
static WebTestClient bindToApplicationContext(ApplicationContext applicationContext);
// Response specification
interface ResponseSpec {
ResponseSpec expectStatus();
ResponseSpec expectHeader();
ResponseSpec expectCookie();
ResponseSpec expectBody();
<T> ResponseSpec expectBody(Class<T> bodyType);
<T> ResponseSpec expectBodyList(Class<T> elementType);
ResponseSpec expectCookies();
// Terminal operations
<T> EntityExchangeResult<T> returnResult(Class<T> bodyType);
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> bodyTypeReference);
EntityExchangeResult<Void> returnResult(Void.class);
}
}// Indicates that transaction should be rolled back after test method
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Rollback {
boolean value() default true;
}
// Indicates that transaction should be committed after test method
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Commit {
}
// Defines SQL scripts to execute before/after test methods
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(SqlGroup.class)
public @interface Sql {
@AliasFor("scripts")
String[] value() default {};
@AliasFor("value")
String[] scripts() default {};
ExecutionPhase executionPhase() default ExecutionPhase.BEFORE_TEST_METHOD;
SqlConfig config() default @SqlConfig;
// Execution phase enum
enum ExecutionPhase {
BEFORE_TEST_METHOD, AFTER_TEST_METHOD
}
}
// Configuration for @Sql
@Target({})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SqlConfig {
String dataSource() default "";
String transactionManager() default "";
TransactionMode transactionMode() default TransactionMode.DEFAULT;
String encoding() default "";
String separator() default ScriptUtils.DEFAULT_STATEMENT_SEPARATOR;
String commentPrefix() default ScriptUtils.DEFAULT_COMMENT_PREFIX;
String blockCommentStartDelimiter() default ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER;
String blockCommentEndDelimiter() default ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER;
ErrorMode errorMode() default ErrorMode.DEFAULT;
// Enums
enum TransactionMode { DEFAULT, ISOLATED, INFERRED }
enum ErrorMode { DEFAULT, FAIL_ON_ERROR, CONTINUE_ON_ERROR, IGNORE_FAILED_DROPS }
}@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
@ActiveProfiles("test")
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService;
@Autowired
private UserRepository userRepository;
@Test
@Transactional
@Rollback
void shouldCreateUserSuccessfully() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email("test@example.com")
.firstName("Test")
.lastName("User")
.build();
// When
User createdUser = userService.createUser(request);
// Then
assertThat(createdUser).isNotNull();
assertThat(createdUser.getId()).isNotNull();
assertThat(createdUser.getUsername()).isEqualTo("testuser");
assertThat(createdUser.getEmail()).isEqualTo("test@example.com");
// Verify email service was called
verify(emailService).sendWelcomeEmail(createdUser);
// Verify user was persisted
Optional<User> savedUser = userRepository.findById(createdUser.getId());
assertThat(savedUser).isPresent();
assertThat(savedUser.get().getUsername()).isEqualTo("testuser");
}
@Test
@Transactional
void shouldThrowExceptionWhenUserAlreadyExists() {
// Given - create user first
User existingUser = User.builder()
.username("duplicate")
.email("duplicate@example.com")
.build();
userRepository.save(existingUser);
CreateUserRequest request = CreateUserRequest.builder()
.username("duplicate")
.email("different@example.com")
.build();
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(UserAlreadyExistsException.class)
.hasMessageContaining("User already exists");
verifyNoInteractions(emailService);
}
}@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldReturnUserWhenValidId() throws Exception {
// Given
Long userId = 1L;
User user = User.builder()
.id(userId)
.username("testuser")
.email("test@example.com")
.build();
when(userService.findById(userId)).thenReturn(user);
// When & Then
mockMvc.perform(get("/api/users/{id}", userId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.username").value("testuser"))
.andExpect(jsonPath("$.email").value("test@example.com"))
.andDo(print());
verify(userService).findById(userId);
}
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
// Given
Long userId = 999L;
when(userService.findById(userId))
.thenThrow(new UserNotFoundException("User not found"));
// When & Then
mockMvc.perform(get("/api/users/{id}", userId))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.message").value("User not found"))
.andExpect(jsonPath("$.code").value("USER_NOT_FOUND"));
}
@Test
void shouldCreateUserWhenValidRequest() throws Exception {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("newuser")
.email("new@example.com")
.firstName("New")
.lastName("User")
.build();
User createdUser = User.builder()
.id(1L)
.username("newuser")
.email("new@example.com")
.firstName("New")
.lastName("User")
.build();
when(userService.createUser(any(CreateUserRequest.class)))
.thenReturn(createdUser);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1))
.andExpected(jsonPath("$.username").value("newuser"));
verify(userService).createUser(any(CreateUserRequest.class));
}
@Test
void shouldReturnBadRequestWhenValidationFails() throws Exception {
// Given
CreateUserRequest invalidRequest = CreateUserRequest.builder()
.username("") // Invalid - empty username
.email("invalid-email") // Invalid email format
.build();
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.validationErrors").exists())
.andExpect(jsonPath("$.validationErrors.username").exists())
.andExpect(jsonPath("$.validationErrors.email").exists());
verifyNoInteractions(userService);
}
@Test
void shouldUpdateUserWhenValidRequest() throws Exception {
// Given
Long userId = 1L;
UpdateUserRequest request = UpdateUserRequest.builder()
.username("updateduser")
.email("updated@example.com")
.build();
User updatedUser = User.builder()
.id(userId)
.username("updateduser")
.email("updated@example.com")
.build();
when(userService.updateUser(eq(userId), any(UpdateUserRequest.class)))
.thenReturn(updatedUser);
// When & Then
mockMvc.perform(put("/api/users/{id}", userId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("updateduser"))
.andExpect(jsonPath("$.email").value("updated@example.com"));
}
@Test
void shouldReturnUsersWithPagination() throws Exception {
// Given
List<User> users = Arrays.asList(
User.builder().id(1L).username("user1").build(),
User.builder().id(2L).username("user2").build()
);
Page<User> userPage = new PageImpl<>(users, PageRequest.of(0, 10), 2);
when(userService.findAll(any(Pageable.class))).thenReturn(userPage);
// When & Then
mockMvc.perform(get("/api/users")
.param("page", "0")
.param("size", "10"))
.andExpected(status().isOk())
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content.length()").value(2))
.andExpect(jsonPath("$.content[0].username").value("user1"))
.andExpect(jsonPath("$.totalElements").value(2))
.andExpect(header().string("X-Total-Count", "2"));
}
}@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void shouldFindUserByUsername() {
// Given
User user = User.builder()
.username("testuser")
.email("test@example.com")
.firstName("Test")
.lastName("User")
.build();
entityManager.persistAndFlush(user);
// When
Optional<User> foundUser = userRepository.findByUsername("testuser");
// Then
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
assertThat(foundUser.get().getEmail()).isEqualTo("test@example.com");
}
@Test
void shouldReturnEmptyWhenUserNotFound() {
// When
Optional<User> foundUser = userRepository.findByUsername("nonexistent");
// Then
assertThat(foundUser).isEmpty();
}
@Test
void shouldFindUsersByEmailDomain() {
// Given
User user1 = User.builder()
.username("user1")
.email("user1@company.com")
.build();
User user2 = User.builder()
.username("user2")
.email("user2@company.com")
.build();
User user3 = User.builder()
.username("user3")
.email("user3@gmail.com")
.build();
entityManager.persist(user1);
entityManager.persist(user2);
entityManager.persist(user3);
entityManager.flush();
// When
List<User> companyUsers = userRepository.findByEmailContaining("@company.com");
// Then
assertThat(companyUsers).hasSize(2);
assertThat(companyUsers).extracting(User::getUsername)
.containsExactlyInAnyOrder("user1", "user2");
}
@Test
@Sql("/test-data/users.sql")
void shouldCountActiveUsers() {
// When
long activeCount = userRepository.countByActiveTrue();
// Then
assertThat(activeCount).isEqualTo(3); // Based on test data
}
@Test
void shouldDeleteUsersByCreatedDateBefore() {
// Given
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
User oldUser = User.builder()
.username("olduser")
.email("old@example.com")
.createdDate(cutoffDate.minusDays(1))
.build();
User newUser = User.builder()
.username("newuser")
.email("new@example.com")
.createdDate(cutoffDate.plusDays(1))
.build();
entityManager.persist(oldUser);
entityManager.persist(newUser);
entityManager.flush();
// When
int deletedCount = userRepository.deleteByCreatedDateBefore(cutoffDate);
// Then
assertThat(deletedCount).isEqualTo(1);
List<User> remainingUsers = userRepository.findAll();
assertThat(remainingUsers).hasSize(1);
assertThat(remainingUsers.get(0).getUsername()).isEqualTo("newuser");
}
}@WebFluxTest(ReactiveUserController.class)
class ReactiveUserControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private ReactiveUserService userService;
@Test
void shouldReturnUserWhenFound() {
// Given
Long userId = 1L;
User user = User.builder()
.id(userId)
.username("testuser")
.email("test@example.com")
.build();
when(userService.findById(userId)).thenReturn(Mono.just(user));
// When & Then
webTestClient.get()
.uri("/api/users/{id}", userId)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(User.class)
.value(returnedUser -> {
assertThat(returnedUser.getId()).isEqualTo(userId);
assertThat(returnedUser.getUsername()).isEqualTo("testuser");
});
}
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() {
// Given
Long userId = 999L;
when(userService.findById(userId)).thenReturn(Mono.empty());
// When & Then
webTestClient.get()
.uri("/api/users/{id}", userId)
.exchange()
.expectStatus().isNotFound();
}
@Test
void shouldCreateUserSuccessfully() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("newuser")
.email("new@example.com")
.build();
User createdUser = User.builder()
.id(1L)
.username("newuser")
.email("new@example.com")
.build();
when(userService.createUser(any(CreateUserRequest.class)))
.thenReturn(Mono.just(createdUser));
// When & Then
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.exchange()
.expectStatus().isCreated()
.expectHeader().exists("Location")
.expectBody(User.class)
.value(user -> {
assertThat(user.getId()).isEqualTo(1L);
assertThat(user.getUsername()).isEqualTo("newuser");
});
}
@Test
void shouldStreamUsers() {
// Given
List<User> users = Arrays.asList(
User.builder().id(1L).username("user1").build(),
User.builder().id(2L).username("user2").build(),
User.builder().id(3L).username("user3").build()
);
when(userService.findAll()).thenReturn(Flux.fromIterable(users));
// When & Then
webTestClient.get()
.uri("/api/users/stream")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM)
.expectBodyList(User.class)
.hasSize(3)
.contains(users.toArray(new User[0]));
}
@Test
void shouldHandleValidationErrors() {
// Given
CreateUserRequest invalidRequest = CreateUserRequest.builder()
.username("") // Invalid
.build();
// When & Then
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(invalidRequest)
.exchange()
.expectStatus().isBadRequest()
.expectBody()
.jsonPath("$.validationErrors").exists()
.jsonPath("$.validationErrors.username").exists();
}
}// Test configuration class
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(Instant.parse("2023-01-01T00:00:00Z"), ZoneOffset.UTC);
}
@Bean
public ObjectMapper testObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
@Bean
@Primary
public PasswordEncoder mockPasswordEncoder() {
return Mockito.mock(PasswordEncoder.class);
}
}
// Base test class with common configuration
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
@Import(TestConfig.class)
@Transactional
public abstract class BaseIntegrationTest {
@Autowired
protected TestEntityManager entityManager;
@MockBean
protected EmailService emailService;
@BeforeEach
void setUp() {
// Common setup
Mockito.reset(emailService);
}
@AfterEach
void tearDown() {
// Common cleanup
}
protected User createTestUser(String username) {
User user = User.builder()
.username(username)
.email(username + "@example.com")
.build();
return entityManager.persistAndFlush(user);
}
}
// Test slices with custom configuration
@WebMvcTest
@Import({SecurityTestConfig.class, TestConfig.class})
class SecureControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdminAccess() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpected(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void shouldDenyUserAccess() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpected(status().isForbidden());
}
}
// Parameterized tests
class UserValidationTest {
@ParameterizedTest
@ValueSource(strings = {"", " ", "ab", "a".repeat(51)})
void shouldRejectInvalidUsernames(String username) {
CreateUserRequest request = CreateUserRequest.builder()
.username(username)
.email("test@example.com")
.build();
Set<ConstraintViolation<CreateUserRequest>> violations = validator.validate(request);
assertThat(violations).isNotEmpty();
assertThat(violations).anyMatch(v -> v.getPropertyPath().toString().equals("username"));
}
@ParameterizedTest
@CsvSource({
"test@example.com, true",
"invalid-email, false",
"@example.com, false",
"test@, false",
"test.email@domain.co.uk, true"
})
void shouldValidateEmailFormats(String email, boolean shouldBeValid) {
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email(email)
.build();
Set<ConstraintViolation<CreateUserRequest>> violations = validator.validate(request);
boolean hasEmailViolation = violations.stream()
.anyMatch(v -> v.getPropertyPath().toString().equals("email"));
assertThat(hasEmailViolation).isEqualTo(!shouldBeValid);
}
}// Performance test with JMH (requires jmh dependency)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@State(Scope.Benchmark)
public class UserServicePerformanceTest {
private UserService userService;
private CreateUserRequest request;
@Setup
public void setup() {
// Initialize Spring context
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
userService = context.getBean(UserService.class);
request = CreateUserRequest.builder()
.username("testuser")
.email("test@example.com")
.build();
}
@Benchmark
public User createUser() {
return userService.createUser(request);
}
}
// Concurrent testing
@SpringBootTest
class ConcurrentUserServiceTest {
@Autowired
private UserService userService;
@Test
void shouldHandleConcurrentUserCreation() throws InterruptedException {
int numberOfThreads = 10;
int usersPerThread = 100;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i;
executor.submit(() -> {
try {
for (int j = 0; j < usersPerThread; j++) {
CreateUserRequest request = CreateUserRequest.builder()
.username("user_" + threadId + "_" + j)
.email("user_" + threadId + "_" + j + "@example.com")
.build();
try {
userService.createUser(request);
successCount.incrementAndGet();
} catch (Exception e) {
errorCount.incrementAndGet();
}
}
} finally {
latch.countDown();
}
});
}
latch.await(30, TimeUnit.SECONDS);
executor.shutdown();
int expectedTotal = numberOfThreads * usersPerThread;
assertThat(successCount.get() + errorCount.get()).isEqualTo(expectedTotal);
assertThat(errorCount.get()).isLessThan(expectedTotal * 0.1); // Less than 10% errors
}
}Spring Test provides comprehensive testing capabilities that enable you to test all layers of your Spring application effectively, from unit tests with mocks to full integration tests with real databases and web servers.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework--spring