javax側が持っているバリデーション(アノテーションで定義出来る)とSpring側のバリデーションのテスト方法について困惑したため、下記の通り纏めます。
ちなみに、javax側のカスタムアノテーションで全て作ればいいじゃんって思うかもしれませんが、思うようにテストが出来なかったため、やむを得ずSpring側で作成しました。
テスト実行時に、自作したValidation内のフィールドへDIする方法
#やりたいこと
まずは対象となるクラスを確認します。
package com.ssp_engine.user.domain.model;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
import com.ssp_engine.user.domain.model.validation.ConfirmPassword;
import com.ssp_engine.user.domain.model.validation.ValidGroup1;
import com.ssp_engine.user.domain.model.validation.ValidGroup2;
import com.ssp_engine.user.domain.model.validation.ValidGroup3;
import com.ssp_engine.user.domain.model.validation.ValidGroup4;
import lombok.Data;
@Data
@ConfirmPassword(field = "password", groups = ValidGroup4.class)
public class PasswordForm {
@NotBlank(groups = ValidGroup1.class)
private String currentPassword;
@NotBlank(groups = ValidGroup1.class)
@Length(min = 4, max = 8, groups = ValidGroup2.class)
@Pattern(regexp="^[a-zA-Z0-9]+$", groups = ValidGroup3.class)
private String password;
@NotBlank(groups = ValidGroup1.class)
private String confirmPassword;
}
こんな感じの、一般的なWebサービスの管理画面にある自身のパスワードを編集する時のフォームクラスです。
currentPassword
は、現在ログイン中のパスワード
password
は、変更するパスワード
confirmPassword
は、確認用パスワード
で、それぞれに@NotBlank
やら@Pattern
の正規表現のバリデーションがあります。
currentPassword
に現在ログインしているパスワードと比較するバリデーションがあり、それが下記の通りです。
package com.ssp_engine.user.domain.model.validation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.ssp_engine.user.domain.model.PasswordForm;
@Component
public class LoginPassAndFormPassValidator implements Validator {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public boolean supports(Class<?> clazz) {
return PasswordForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
PasswordForm form = (PasswordForm) target;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails principal = (UserDetails) auth.getPrincipal();
String userPass = principal.getPassword();
if (form.getCurrentPassword() == null) {
return;
}
if (!this.passwordEncoder.matches(form.getCurrentPassword(), userPass)) {
errors.rejectValue("currentPassword",
"LoginPassAndFormPassValidator.PasswordForm.currentPassword",
"ログイン中のパスワードと異なります。");
}
}
}
コントローラー側で、 @InitBinder
してから使用しています。
これらのSpring側とJavax側のバリデーターをテストしていきます。
##テスト
そして、テストですが、コントローラー側のテストとフォームクラスのテストで分けたほうが長くならず、テスト内容を切り分けやすいとのことで、こんな感じになりました。
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordFormTests {
private PasswordForm passwordForm = new PasswordForm();
private BindingResult bindingResult = new BindException(passwordForm, "PasswordForm"); //①
@Autowired
@Qualifier("loginPassAndFormPassValidator") //②
/* Spring側 */
private org.springframework.validation.Validator loginPassAndFormPassValidator;
/* javax側 */
private static Validator validator; //③
@BeforeClass
public static void 初期化処理() { //④
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Before
public void 値をセット() throws Exception{ //⑤
this.passwordForm.setCurrentPassword("currentpassword");
this.passwordForm.setConfirmPassword("password");
this.passwordForm.setPassword("password");
}
}
①・・・バリデーターを実行した後に、結果を受け取るためのフィールド
②・・・明示的にどのクラスかを指定してあげる必要があったため、@Qualifier
で指定しました。
③・④・・・@AutoWired
で指定して、Beanをゲットしたかったのですが、上手く行かなかったので、明示的にBeanを作っています。
⑤・・・対象となるオブジェクトに値をセットしています。
準備が整ったところで、ゴリゴリテストをしていくわけですが、こんな感じになりました。
まずは、エラーが出ないことを確認。
@Test
@WithMockUser(username = "username",
password ="$2a$10$p3/Malw3/KWyfOlPwWoUCulx4iDb2C/nmo6x8P2svXjfJQ5ETLhG2",
roles = "USER")
public void エラー無し() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult); //①
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class,ValidGroup2.class,ValidGroup3.class,ValidGroup4.class); //②
assertThat(bindingResult.getFieldError(), is(nullValue())); //③
assertThat(violations.size(), is(0)); //④
}
①・・・Spring側のバリデーションを実行しています。第一引数に対象のオブジェクト、第二引数に、結果を格納するオブジェクトbindingResult
を指定してあげています。
②・・・ConstraintViolation
は制約違反の内容を格納したオブジェクトのセットを返し、validateの第一引数には、対象のオブジェクト。第二引数には、ValidGroupを指定していたため、どのバリデーションを有効にするか指定しています。
②・・・Spring側の結果をbindingResult
で受け取り、Nullかどうかチェックしています。
③・・・Javax側の結果をviolations.size()
でサイズを図り、0かチェックしています。
@WithMockUser
は、loginPassAndFormPassValidator
でログイン情報を取得する必要があったため、ログイン状態にしています。
これで、エラーが出ないことが分かったら、こんな感じで書いていきました。
@Test
@WithMockUser(username = "username",
password ="currentpassword",
roles = "USER")
public void ログインパスと入力パスが違う() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
assertThat(bindingResult.getFieldError().getDefaultMessage(), is("ログイン中のパスワードと異なります。"));
}
@Test
public void 現在のパスワードがBlank() throws Exception{
this.passwordForm.setCurrentPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "currentPassword"), is(instanceOf(NotBlank.class))); //①
}
private Annotation getAnnotation(Set<ConstraintViolation<PasswordForm>> violations, String path) { //②
return violations.stream()
.filter(cv -> cv.getPropertyPath().toString().equals(path))
.findFirst()
.map(cv -> cv.getConstraintDescriptor().getAnnotation())
.get();
}
①・・・どのアノテーションでエラーが出てるか確認しています。
②・・・エラーで弾かれたアノテーションのインスタンスを取得するために、メソッドを作っています。
#全体像
全体像はこんな感じになりました。
package com.ssp_engine.user.domain.model;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.lang.annotation.Annotation;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import com.ssp_engine.user.domain.model.validation.ConfirmPassword;
import com.ssp_engine.user.domain.model.validation.ValidGroup1;
import com.ssp_engine.user.domain.model.validation.ValidGroup2;
import com.ssp_engine.user.domain.model.validation.ValidGroup3;
import com.ssp_engine.user.domain.model.validation.ValidGroup4;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordFormTests {
private PasswordForm passwordForm = new PasswordForm();
private BindingResult bindingResult = new BindException(passwordForm, "PasswordForm");
@Autowired
@Qualifier("loginPassAndFormPassValidator")
/* Spring側 */
private org.springframework.validation.Validator loginPassAndFormPassValidator;
/* javax側 */
private static Validator validator;
@BeforeClass
public static void 初期化処理() {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Before
public void 値をセット() throws Exception{
this.passwordForm.setCurrentPassword("currentpassword");
this.passwordForm.setConfirmPassword("password");
this.passwordForm.setPassword("password");
}
@Test
@WithMockUser(username = "username",
password ="$2a$10$p3/Malw3/KWyfOlPwWoUCulx4iDb2C/nmo6x8P2svXjfJQ5ETLhG2",
roles = "USER")
public void エラー無し() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class,ValidGroup2.class,ValidGroup3.class,ValidGroup4.class);
assertThat(bindingResult.getFieldError(), is(nullValue()));
assertThat(violations.size(), is(0));
}
@Test
@WithMockUser(username = "username",
password ="currentpassword",
roles = "USER")
public void ログインパスと入力パスが違う() throws Exception{
loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
assertThat(bindingResult.getFieldError().getDefaultMessage(), is("ログイン中のパスワードと異なります。"));
}
@Test
public void 現在のパスワードがBlank() throws Exception{
this.passwordForm.setCurrentPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "currentPassword"), is(instanceOf(NotBlank.class)));
}
@Test
public void パスワードがBlank() throws Exception{
this.passwordForm.setPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(NotBlank.class)));
}
@Test
public void 確認用パスワードがBlank() throws Exception{
this.passwordForm.setConfirmPassword("");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup1.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "confirmPassword"), is(instanceOf(NotBlank.class)));
}
@Test
public void 確認用パスワードと入力パスワードが異なる時() throws Exception{
this.passwordForm.setPassword("aiueo");
this.passwordForm.setConfirmPassword("kakikukeko");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup4.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(ConfirmPassword.class)));
}
@Test
public void パスワードが8文字以上の時() throws Exception{
this.passwordForm.setPassword("aiueokakikukeko");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup2.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Length.class)));
}
@Test
public void パスワードが4文字以下の時() throws Exception{
this.passwordForm.setPassword("aiu");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup2.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Length.class)));
}
@Test
public void パスワードが半角英数字以外の時() throws Exception{
this.passwordForm.setPassword("テストです");
Set<ConstraintViolation<PasswordForm>> violations =
validator.validate(this.passwordForm,ValidGroup3.class);
assertThat(violations.size(), is(1));
assertThat(getAnnotation(violations, "password"), is(instanceOf(Pattern.class)));
}
private Annotation getAnnotation(Set<ConstraintViolation<PasswordForm>> violations, String path) {
return violations.stream()
.filter(cv -> cv.getPropertyPath().toString().equals(path))
.findFirst()
.map(cv -> cv.getConstraintDescriptor().getAnnotation())
.get();
}
}
参考になった方がいらっしゃいましたら、幸いです。