[JAVA] Form class validation test with Spring Boot

I was confused about the validation (which can be defined by annotation) on the javax side and the validation test method on the Spring side, so I will summarize it as follows. By the way, you may think that you should make everything with custom annotations on the javax side, but I could not test it as I expected, so I had to make it on the Spring side.

How to DI to the field in the validation that I made when executing the test

Thing you want to do

First, check the target class.

PasswordForm.java


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;

}

It is a form class when editing your own password on the management screen of a general Web service like this. currentPassword is the password you are currently logged in to password is the password to change confirmPassword is the confirmation password

So, each has a regular expression validation for @ NotBlank and @Pattern. There is a validation in currentPassword to compare with the currently logged in password, which is as follows.

LoginPassAndFormPassValidator.java


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",
					           "It is different from the password you are logged in to.");
        }
    }
}

On the controller side, I use it after @InitBinder. We will test these Spring and Javax validators.

test

And as for the test, it is not long to separate the test on the controller side and the test of the form class, and it is easier to separate the test contents, so it became like this.

PasswordFormTests.java



@RunWith(SpringRunner.class)
@SpringBootTest
public class PasswordFormTests {

    private PasswordForm passwordForm = new PasswordForm();
    private BindingResult bindingResult = new BindException(passwordForm, "PasswordForm"); //①

    @Autowired
    @Qualifier("loginPassAndFormPassValidator") //②
    /*Spring side*/
	private org.springframework.validation.Validator loginPassAndFormPassValidator;
    /*javax side*/
    private static Validator validator; //③

    @BeforeClass
public static void initialization process() { //④
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); 
        validator = validatorFactory.getValidator(); 
    }

    @Before
public void Set value() throws Exception{ //⑤
    	this.passwordForm.setCurrentPassword("currentpassword");
    	this.passwordForm.setConfirmPassword("password");
    	this.passwordForm.setPassword("password");
    }

}

① ・ ・ ・ Field for receiving the result after executing the validator ② ・ ・ ・ Since it was necessary to explicitly specify which class, it was specified with @Qualifier. ③ ・ ④ ・ ・ ・ I wanted to get a bean by specifying it with @AutoWired, but it didn't work, so I'm explicitly creating a bean. ⑤ ・ ・ ・ A value is set for the target object.

When I'm ready, I'm going to do a gorigori test, and it looks like this. First, check that there are no errors.

PasswordFormTests.java


    @Test
    @WithMockUser(username = "username",
    			  password ="$2a$10$p3/Malw3/KWyfOlPwWoUCulx4iDb2C/nmo6x8P2svXjfJQ5ETLhG2",
    			  roles = "USER")
public void no error() 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)); //④
    }

① ・ ・ ・ Validation on the Spring side is being executed. The target object is specified in the first argument, and the object bindingResult that stores the result is specified in the second argument. ② ・ ・ ・ ConstraintViolation returns a set of objects that store the contents of the constraint violation, and the target object is the first argument of validate. Since ValidGroup was specified in the second argument, which validation is enabled is specified. ② ・ ・ ・ Receives the result on the Spring side with bindingResult and checks whether it is Null. ③ ・ ・ ・ The result on the Javax side is sized with violations.size () and checked to see if it is 0.

@WithMockUser is logged in because it was necessary to get login information with loginPassAndFormPassValidator.

When I found out that there was no error with this, I wrote it like this.

PasswordFormTests.java


    @Test
    @WithMockUser(username = "username",
    			  password ="currentpassword",
    			  roles = "USER")
public void Login path and input path are different() throws Exception{
    	loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
        assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
        assertThat(bindingResult.getFieldError().getDefaultMessage(), is("It is different from the password you are logged in to."));
    }

    @Test
public void The current password is 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();
    }

① ・ ・ ・ Check which annotation causes the error. (2) ... I am creating a method to get an instance of the annotation that was played with an error.

Overall picture

The whole picture looks like this.

PasswordFormTests.java



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 side*/
	private org.springframework.validation.Validator loginPassAndFormPassValidator;
    /*javax side*/
    private static Validator validator;

    @BeforeClass
public static void initialization process() {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Before
public void Set value() 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 no error() 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 Login path and input path are different() throws Exception{
    	loginPassAndFormPassValidator.validate(this.passwordForm, bindingResult);
        assertThat(bindingResult.getFieldError("currentPassword"), is(bindingResult.getFieldError()));
        assertThat(bindingResult.getFieldError().getDefaultMessage(), is("It is different from the password you are logged in to."));
    }

    @Test
public void The current password is 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 Password is 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 Confirmation password is 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 When the confirmation password and the input password are different() 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 When the password is 8 characters or more() 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 When the password is 4 characters or less() 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 When the password is other than half-width alphanumeric characters() throws Exception{
    	this.passwordForm.setPassword("It's a test");
        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();
    }
}

I would be grateful if anyone could find it helpful.

Recommended Posts

Form class validation test with Spring Boot
Get validation results with Spring Boot
Spring Boot Form
Perform transaction confirmation test with Spring Boot
Download with Spring Boot
Test controller with Mock MVC in Spring Boot
[Java] Article to add validation with Spring Boot 2.3.1.
Generate barcode with Spring Boot
Hello World with Spring Boot
Get started with Spring boot
[JUnit 5 compatible] Write a test using JUnit 5 with Spring boot 2.2, 2.3
Hello World with Spring Boot!
Run LIFF with Spring Boot
SNS login with Spring Boot
Spring Boot starting with Docker
Test field-injected class in Spring boot test without using Spring container
Hello World with Spring Boot
Set cookies with Spring Boot
Use Spring JDBC with Spring Boot
Add module with Spring Boot
Getting Started with Spring Boot
Create microservices with Spring Boot
Spring single item validation test
Send email with spring boot
Spring Boot validation message changes
Use Basic Authentication with Spring Boot
gRPC on Spring Boot with grpc-spring-boot-starter
Hot deploy with Spring Boot development
Write test code in Spring Boot
Spring Boot programming with VS Code
Until "Hello World" with Spring Boot
Inquiry application creation with Spring Boot
(Intellij) Hello World with Spring Boot
Google Cloud Platform with Spring Boot 2.0.0
Use DBUnit for Spring Boot test
Check date correlation with Spring Boot
I tried GraphQL with Spring Boot
[Java] LINE integration with Spring Boot
Beginning with Spring Boot 0. Use Spring CLI
I tried Flyway with Spring Boot
Test Spring framework controller with Junit
Message cooperation started with Spring Boot
Spring Boot gradle build with Docker
I made a simple search form with Spring Boot + GitHub Search API.
Sample code to unit test a Spring Boot controller with MockMvc
[Spring Boot] Until @Autowired is run in the test class [JUnit5]
Processing at application startup with Spring Boot
Hello World with Eclipse + Spring Boot + Maven
Send regular notifications with LineNotify + Spring Boot
HTTPS with Spring Boot and Let's Encrypt
Try using Spring Boot with VS Code
Spring Boot @WebMvcTest test enables Spring Security default security
Start web application development with Spring Boot
I tried Lazy Initialization with Spring Boot 2.2.0
Implement CRUD with Spring Boot + Thymeleaf + MySQL
Asynchronous processing with Spring Boot using @Async
Implement paging function with Spring Boot + Thymeleaf
(IntelliJ + gradle) Hello World with Spring Boot
Use cache with EhCashe 2.x with Spring Boot
Run WEB application with Spring Boot + Thymeleaf
Achieve BASIC authentication with Spring Boot + Spring Security