tutorial about testing web controllers

This commit is contained in:
Tom Hombergs
2019-01-21 05:20:05 +01:00
parent 5352631522
commit 8d8937c6bf
18 changed files with 443 additions and 18 deletions

View File

@@ -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/)

View File

@@ -1,7 +0,0 @@
package io.reflectoring.testing;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.testing.domain;
public interface SaveUserPort {
Long saveUser(User user);
}

View File

@@ -1,4 +1,4 @@
package io.reflectoring.testing;
package io.reflectoring.testing.domain;
import javax.persistence.Entity;
import javax.persistence.Id;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
package io.reflectoring.testing.persistence;
import org.springframework.data.repository.CrudRepository;
public interface UserEntityRepository extends CrudRepository<UserEntity, Long> {
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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> {

View File

@@ -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"));
}
}

View File

@@ -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();
}
}