tutorial about testing web controllers
This commit is contained in:
@@ -3,3 +3,4 @@
|
||||
## Blog Posts
|
||||
* [Structuring and Testing Modules and Layers with Spring Boot](https://reflectoring.io/testing-verticals-and-layers-spring-boot/)
|
||||
* [All You Need To Know About Unit Testing with Spring Boot](https://reflectoring.io/unit-testing-spring-boot/)
|
||||
* [All You Need To Know About Testing Web Controllers with Spring Boot](https://reflectoring.io/spring-boot-web-controller-test/)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.reflectoring.testing;
|
||||
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface UserRepository extends CrudRepository<User, Long> {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.reflectoring.testing;
|
||||
package io.reflectoring.testing.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -9,11 +9,11 @@ import org.springframework.stereotype.Service;
|
||||
@RequiredArgsConstructor
|
||||
public class RegisterUseCase {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final SaveUserPort saveUserPort;
|
||||
|
||||
public User registerUser(User user) {
|
||||
public Long registerUser(User user, boolean sendWelcomeMail) {
|
||||
user.setRegistrationDate(LocalDateTime.now());
|
||||
return userRepository.save(user);
|
||||
return saveUserPort.saveUser(user);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.reflectoring.testing.domain;
|
||||
|
||||
public interface SaveUserPort {
|
||||
|
||||
Long saveUser(User user);
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.reflectoring.testing;
|
||||
package io.reflectoring.testing.domain;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.reflectoring.testing.persistence;
|
||||
|
||||
import io.reflectoring.testing.domain.SaveUserPort;
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PersistenceAdapter implements SaveUserPort {
|
||||
|
||||
private final UserEntityRepository userEntityRepository;
|
||||
|
||||
@Override
|
||||
public Long saveUser(User user) {
|
||||
UserEntity userEntity = new UserEntity(
|
||||
user.getName(),
|
||||
user.getEmail());
|
||||
UserEntity savedUserEntity = userEntityRepository.save(userEntity);
|
||||
return savedUserEntity.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.reflectoring.testing.persistence;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class UserEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
private LocalDateTime registrationDate;
|
||||
|
||||
public UserEntity(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.reflectoring.testing.persistence;
|
||||
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface UserEntityRepository extends CrudRepository<UserEntity, Long> {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ControllerAdvice
|
||||
public class ControllerExceptionHandler {
|
||||
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseBody
|
||||
public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
ErrorResult errorResult = new ErrorResult();
|
||||
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
|
||||
errorResult.getFieldErrors()
|
||||
.add(new FieldValidationError(fieldError.getField(), fieldError.getDefaultMessage()));
|
||||
}
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
@NoArgsConstructor
|
||||
public class ErrorResult {
|
||||
private final List<FieldValidationError> fieldErrors = new ArrayList<>();
|
||||
|
||||
ErrorResult(String field, String message) {
|
||||
this.fieldErrors.add(new FieldValidationError(field, message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FieldValidationError {
|
||||
|
||||
private String field;
|
||||
|
||||
private String message;
|
||||
|
||||
public FieldValidationError(@JsonProperty("field") String field, @JsonProperty("message") String message) {
|
||||
this.field = field;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import io.reflectoring.testing.domain.RegisterUseCase;
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
class RegisterMvcController {
|
||||
|
||||
private final RegisterUseCase registerUseCase;
|
||||
|
||||
@PostMapping("/mvc/register/")
|
||||
ModelAndView register(@ModelAttribute("user") UserResource userResource){
|
||||
|
||||
User user = new User(
|
||||
userResource.getName(),
|
||||
userResource.getEmail());
|
||||
|
||||
registerUseCase.registerUser(user, false);
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView("registration_success.html");
|
||||
modelAndView.addObject("username", user.getName());
|
||||
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import io.reflectoring.testing.domain.RegisterUseCase;
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
class RegisterRestController {
|
||||
|
||||
private final RegisterUseCase registerUseCase;
|
||||
|
||||
@PostMapping("/forums/{forumId}/register")
|
||||
UserResource register(
|
||||
@PathVariable("forumId") Long forumId,
|
||||
@Valid @RequestBody UserResource userResource,
|
||||
@RequestParam("sendWelcomeMail") boolean sendWelcomeMail) {
|
||||
|
||||
User user = new User(
|
||||
userResource.getName(),
|
||||
userResource.getEmail());
|
||||
Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
|
||||
|
||||
return new UserResource(
|
||||
user.getName(),
|
||||
user.getEmail());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class UserResource {
|
||||
|
||||
@NotNull
|
||||
private final String name;
|
||||
|
||||
@NotNull
|
||||
private final String email;
|
||||
|
||||
private LocalDateTime registrationDate;
|
||||
|
||||
public UserResource(
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("email") String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.registrationDate = null;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package io.reflectoring.testing;
|
||||
|
||||
import io.reflectoring.testing.domain.RegisterUseCase;
|
||||
import io.reflectoring.testing.domain.SaveUserPort;
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import static io.reflectoring.testing.UserAssert.assertThat;
|
||||
import static org.assertj.core.api.Java6Assertions.*;
|
||||
import static org.mockito.AdditionalAnswers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -14,21 +17,21 @@ import static org.mockito.Mockito.*;
|
||||
class RegisterUseCaseTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
private SaveUserPort saveUserPort;
|
||||
|
||||
private RegisterUseCase registerUseCase;
|
||||
|
||||
@BeforeEach
|
||||
void initUseCase() {
|
||||
registerUseCase = new RegisterUseCase(userRepository);
|
||||
registerUseCase = new RegisterUseCase(saveUserPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savedUserHasRegistrationDate() {
|
||||
User user = new User("zaphod", "zaphod@mail.com");
|
||||
when(userRepository.save(any(User.class))).then(returnsFirstArg());
|
||||
User savedUser = registerUseCase.registerUser(user);
|
||||
assertThat(savedUser).hasRegistrationDate();
|
||||
when(saveUserPort.saveUser(any(User.class))).then(returnsFirstArg());
|
||||
Long userId = registerUseCase.registerUser(user, false);
|
||||
assertThat(userId).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.reflectoring.testing;
|
||||
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
public class UserAssert extends AbstractAssert<UserAssert, User> {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.reflectoring.testing.domain.RegisterUseCase;
|
||||
import io.reflectoring.testing.domain.User;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import static io.reflectoring.testing.web.ResponseBodyMatchers.*;
|
||||
import static org.assertj.core.api.Java6Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebMvcTest(controllers = RegisterRestController.class)
|
||||
class RegisterRestControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private RegisterUseCase registerUseCase;
|
||||
|
||||
@Test
|
||||
void whenValidInput_thenReturns200() throws Exception {
|
||||
|
||||
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
|
||||
|
||||
mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenValidInput_thenMapsToBusinessModel() throws Exception {
|
||||
|
||||
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
|
||||
|
||||
mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
verify(registerUseCase, times(1)).registerUser(userCaptor.capture(), eq(true));
|
||||
assertThat(userCaptor.getValue().getName()).isEqualTo("Zaphod");
|
||||
assertThat(userCaptor.getValue().getEmail()).isEqualTo("zaphod@galaxy.net");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenValidInput_thenReturnsUserResource() throws Exception {
|
||||
|
||||
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
UserResource expectedResponseBody = user;
|
||||
String actualResponseBody = mvcResult.getResponse().getContentAsString();
|
||||
assertThat(objectMapper.writeValueAsString(expectedResponseBody))
|
||||
.isEqualToIgnoringWhitespace(actualResponseBody);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenValidInput_thenReturnsUserResource_withFluentApi() throws Exception {
|
||||
|
||||
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
|
||||
UserResource expectedResponseBody = user;
|
||||
|
||||
mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(responseBody().containsObjectAsJson(expectedResponseBody, UserResource.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenValidInput_thenReturnsUserResource_withTypedAssertion() throws Exception {
|
||||
|
||||
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
UserResource expectedResponseBody = user;
|
||||
UserResource actualResponseBody = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), UserResource.class);
|
||||
assertThat(expectedResponseBody.getName()).isEqualTo(actualResponseBody.getName());
|
||||
assertThat(expectedResponseBody.getEmail()).isEqualTo(actualResponseBody.getEmail());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNullValue_thenReturns400() throws Exception {
|
||||
|
||||
UserResource user = new UserResource(null, "zaphod@galaxy.net");
|
||||
|
||||
mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNullValue_thenReturns400AndErrorResult() throws Exception {
|
||||
|
||||
UserResource user = new UserResource(null, "zaphod@galaxy.net");
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
|
||||
ErrorResult expectedErrorResponse = new ErrorResult("name", "must not be null");
|
||||
String actualResponseBody = mvcResult.getResponse().getContentAsString();
|
||||
String expectedResponseBody = objectMapper.writeValueAsString(expectedErrorResponse);
|
||||
assertThat(expectedResponseBody).isEqualToIgnoringWhitespace(actualResponseBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNullValue_thenReturns400AndErrorResult_withFluentApi() throws Exception {
|
||||
|
||||
UserResource user = new UserResource(null, "zaphod@galaxy.net");
|
||||
|
||||
mockMvc.perform(post("/forums/{forumId}/register", 42L)
|
||||
.contentType("application/json")
|
||||
.param("sendWelcomeMail", "true")
|
||||
.content(objectMapper.writeValueAsString(user)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(responseBody().containsErrorMessageForField("name", "must not be null"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.reflectoring.testing.web;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import static org.assertj.core.api.Java6Assertions.*;
|
||||
|
||||
public class ResponseBodyMatchers {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public <T> ResultMatcher containsObjectAsJson(Object expectedObject, Class<T> targetClass) {
|
||||
return mvcResult -> {
|
||||
String json = mvcResult.getResponse().getContentAsString();
|
||||
T actualObject = objectMapper.readValue(json, targetClass);
|
||||
assertThat(expectedObject).isEqualToComparingFieldByField(actualObject);
|
||||
};
|
||||
}
|
||||
|
||||
public ResultMatcher containsErrorMessageForField(String expectedFieldName, String expectedMessage) {
|
||||
return mvcResult -> {
|
||||
String json = mvcResult.getResponse().getContentAsString();
|
||||
ErrorResult errorResult = objectMapper.readValue(json, ErrorResult.class);
|
||||
List<FieldValidationError> fieldErrors = errorResult.getFieldErrors().stream()
|
||||
.filter(fieldError -> fieldError.getField().equals(expectedFieldName))
|
||||
.filter(fieldError -> fieldError.getMessage().equals(expectedMessage))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertThat(fieldErrors)
|
||||
.hasSize(1)
|
||||
.withFailMessage("expecting exactly 1 error message with field name '%s' and message '%s'",
|
||||
expectedFieldName,
|
||||
expectedMessage);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static ResponseBodyMatchers responseBody() {
|
||||
return new ResponseBodyMatchers();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user