Статьи

Spring MVC Integration Testing: Утверждают, что у заданных атрибутов модели есть глобальные ошибки

Логотип-весна-ю

Чтобы сообщить о глобальной ошибке в Spring MVC с помощью Bean Validation, мы можем создать пользовательскую аннотацию ограничения уровня класса. Глобальные ошибки не связаны с какими-либо конкретными полями в проверенном компоненте. В этой статье я покажу, как написать тест с Spring Test, который проверяет, есть ли у данного атрибута модели глобальные ошибки проверки.

Пользовательское (уровень класса) ограничение

Ради этой статьи я создал сравнительно простое ограничение на уровне класса под названием SamePassword , проверенное SamePasswordValidator :

1
2
3
4
5
6
7
8
9
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = SamePasswordsValidator.class)
@Documented
public @interface SamePasswords {
    String message() default "passwords do not match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Как вы можете видеть ниже, валидатор действительно прост:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class SamePasswordsValidator implements ConstraintValidator<SamePasswords, PasswordForm> {
 
    @Override
    public void initialize(SamePasswords constraintAnnotation) {}
 
    @Override
    public boolean isValid(PasswordForm value, ConstraintValidatorContext context) {
        if(value.getConfirmedPassword() == null) {
            return true;
        }
        return value.getConfirmedPassword()
                    .equals(value.getPassword());
    }
}

PasswordForm — это просто POJO с некоторыми аннотациями ограничений, включая тот, который я только что создал:

01
02
03
04
05
06
07
08
09
10
@SamePasswords
public class PasswordForm {
    @NotBlank
    private String password;
    @NotBlank
    private String confirmedPassword;
 
    // getters and setters omitted for redability
 
}

@Controller

У контроллера есть два метода: отобразить форму и обработать отправку формы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@Controller
@RequestMapping("globalerrors")
public class PasswordController {
 
    @RequestMapping(value = "password")
    public String password(Model model) {
        model.addAttribute(new PasswordForm());
        return "globalerrors/password";
    }
 
    @RequestMapping(value = "password", method = RequestMethod.POST)
    public String stepTwo(@Valid PasswordForm passwordForm, Errors errors) {
        if (errors.hasErrors()) {
            return "globalerrors/password";
        }
        return "redirect:password";
    }
}

При сбое проверки пароля глобальная ошибка регистрируется в BindingResult ( Errors в приведенном выше примере). Затем мы можем отобразить эту ошибку в верхней части формы, например, на странице HTML. В Thymeleaf это будет:

1
2
3
<div th:if="${#fields.hasGlobalErrors()}">
  <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
</div>

Интеграционное тестирование с Spring Test

Давайте настроим интеграционный тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class AccountValidationIntegrationTest {
 
    @Autowired
    private WebApplicationContext wac;
    private MockMvc mockMvc;
 
    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
}

Первый тест проверяет, что отправка формы с пустым password и confirmedPassword не удалась:

01
02
03
04
05
06
07
08
09
10
11
12
@Test
    public void failsWhenEmptyPasswordsGiven() throws Exception {
        this.mockMvc.perform(post("/globalerrors/password")
                .param("password", "").param("confirmedPassword", ""))
                .andExpect(
                    model().attributeHasFieldErrors(
                        "passwordForm", "password", "confirmedPassword"
                    )
                )
                .andExpect(status().isOk())
                .andExpect(view().name("globalerrors/password"));
    }

В приведенном выше примере тест проверяет наличие ошибок в полях как password поля password и для confirmedPassword password .

Точно так же я хотел бы проверить, что, когда данные пароли не совпадают, я получаю конкретную глобальную ошибку. Поэтому я ожидал бы что-то вроде этого: .andExpect(model().hasGlobalError("passwordForm", "passwords do not match")) . К сожалению, ModelResultMatchers возвращаемый MockMvcResultMatchers#model() , не предоставляет методы для утверждения, что у данного атрибута (ов) модели есть глобальные ошибки.

Поскольку его там нет, я создал свой собственный ModelResultMatchers сопоставления, ModelResultMatchers . Версия кода для Java 8 приведена ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class GlobalErrorsMatchers extends ModelResultMatchers {
 
    private GlobalErrorsMatchers() {
    }
 
    public static GlobalErrorsMatchers globalErrors() {
        return new GlobalErrorsMatchers();
    }
 
    public ResultMatcher hasGlobalError(String attribute, String expectedMessage) {
        return result -> {
            BindingResult bindingResult = getBindingResult(
                result.getModelAndView(), attribute
            );
            bindingResult.getGlobalErrors()
                .stream()
                .filter(oe -> attribute.equals(oe.getObjectName()))
                .forEach(oe -> assertEquals(
                    "Expected default message", expectedMessage, oe.getDefaultMessage())
                );
        };
    }
 
    private BindingResult getBindingResult(ModelAndView mav, String name) {
        BindingResult result = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);
        assertTrue(
            "No BindingResult for attribute: " + name, result != null
        );
        assertTrue(
            "No global errors for attribute: " + name, result.getGlobalErrorCount() > 0
        );
        return result;
    }
}

С помощью вышеуказанного дополнения я теперь могу проверить глобальные ошибки валидации, как здесь ниже:

01
02
03
04
05
06
07
08
09
10
11
12
import static pl.codeleak.demo.globalerrors.GlobalErrorsMatchers.globalErrors;
 
@Test
public void failsWithGlobalErrorWhenDifferentPasswordsGiven() throws Exception {
    this.mockMvc.perform(post("/globalerrors/password")
            .param("password", "test").param("confirmedPassword", "other"))
            .andExpect(globalErrors().hasGlobalError(
                "passwordForm", "passwords do not match")
            )
            .andExpect(status().isOk())
            .andExpect(view().name("globalerrors/password"));
}

Как вы можете видеть, расширение сопоставителей Spring Test и предоставление собственных сравнительно легко, и его можно использовать для улучшения проверки валидации в интеграционном тесте.

Ресурсы