ConstraintsViolationException not getting triggered between 2 methods in the same Service Layer

83 views Asked by At
@Service
@Validated
public class ValidateService {

    public void testValidation(String notValidated) {
        testValidationNotNull(notValidated);
    }

    public void testValidationNotNull(@NotNull String validated) {
        validated.isBlank();
    }

}

I am expecting to get ConstraintViolationException when testValidation method is executed and notValidated string is null, but instead i get NPE since validated.isBlank() row is getting executed.

Is this the wanted bahavior from javax.validation.constraints.NotNull when the method is being called from another method in the same service layer?

Below are 2 test methods: 1 triggers NPE, 2 triggers ConstraintViolationException

@SpringBootTest
public class ValidateServiceTest {

    @Autowired
    private ValidateService sut;

    // test that triggers NPE instead of ConstraintViolationException
    @Test
    public void testValidation() {
        sut.testValidation(null);
    }

    // green test which triggers ConstraintViolationException 
    @Test
    public void testValidationNotNull_shouldThrowConstraintsViolationException() {
        final ConstraintViolationException violationException = assertThrows(ConstraintViolationException.class, () ->  sut.testValidationNotNull(null));
        assertEquals("testValidationNotNull.validated: must not be null", violationException.getMessage());
    }

}
2

There are 2 answers

17
Andrei Lisa On

Here you are passing null as a method argument and after it you are trying to invoke isBlank() from String but if you are going to check the java docs of String class, before of all there is writing.

` * Unless otherwise noted, passing a {@code null} argument to a constructor
 * or method in this class will cause a {@link NullPointerException} to be
 * thrown.`

it is the reason.

By the way I recommend you to check all answers of this question to have more details why. There also is present some good references to documentation.

The reason is simple:

Annotation has no effect because there’s no validator to enforce it. in another way :

@NotNull is just an annotation. Annotations do nothing on their own. They need an annotation processor at compile time, or something that processes it at runtime.

It is the reason why you got NullPointerException to fix it you also should invoke ConstraintViolationException as in second test-case

0
Ishan On

From a ValidateServiceTest instance itself from one method testValidation(String) another method this.testValidationNotNull(String) is called, it does not process any annotation. This is a direct call from one method to the other. There is NO interceptor to process annotations. From one BEAN to another Autowired Bean if you call a method, then some framework comes into the picture and it intercepts that call and processes annotations like @Transactional, @NotNull, etc. But within a class, from one method to another method, nothing will happen.

Your 2nd test calls sut.testValidationNotNull(null) where sut is an autowired bean. So, @NotNull is effective. But not from inside ValidateService instance itself when you call this.testValidationNotNull from testValidation

When you want to enforce validation, call the method that has @NotNull from outside the Bean through an autowired (managed) instance. That's the only possibility. Remember, whenever from within the bean itself, if you call any other method on the same instance - that will get called directly - without any framework interception coming into the picture.

I don't know the exact usecase, but from the looks of it, I believe you should not have method testValidation at all. Just have public void testValidationNotNull(@NotNull String validated) which always gets called from the outside (on a managed bean instance) and @NotNull will always be processed. Framework validates the parameter BEFORE actually reaching your method testValidationNotNull and raises constraint violation exception. INSIDE your method testValidationNotNull, you are 100% sure that the parameter is NOT NULL.

You can have @Autowired smallBean in the large bean. Then call smallBean.someSmallMethod(String) - which would process annotations before calling someSmallMethod.

Then have @Autowired smallerBean in the smallBean class. Call smallerBean.someSmallerMethod(String) - which would process annotations before actually reaching someSmallerMethod.