Bean Validation: How can I manually create a ConstraintViolation?
Asked Answered
G

5

19

I have a specific scenario where I can only check for violation conditions manually, at a later point in the flow.

What I want to do is throw a ConstraintViolationException, and provide a "real" ConstraintViolation object to it (when I catch the exception up the stack, I use the #{validatedValue} and violation.getPropertyPath() parameters).

How can I create a ConstraintViolation myself without having the framework do it for me via annotations (I use Hibernate Validator)?

Code example:

List<String> columnsListForSorting = new ArrayList<String>(service.getColumnsList(domain));
Collections.sort(columnsListForSorting);

String firstFieldToSortBy = this.getTranslatedFieldName(domain.getClass().getCanonicalName(), sortingInfo.getSortedColumn());
if (!columnsListForSorting.contains(firstFieldToSortBy)){
    throw new ConstraintViolationException(<what here?...>);
}

Thanks.

Globose answered 8/4, 2014 at 13:21 Comment(6)
What is your test code so far?Yautia
Edit your question, add the snippet and format it with the "code" button, it makes it easier to read :)Yautia
Thanks, original question has been updated.Globose
Based on your snippet, why not throw and catch a custom exception to better express what is happening throw new MissingFilterField(firstFieldToSortBy)?Yautia
Because up the stack, a ConstraintViolation exception is caught that already translates the exception to the expected error return value for the customer. But I get what you're saying. Perhaps I should just create a new exception handling scenario.Globose
Ok then, I assume the snippet is from a JUnit test and you're mocking some call which in production will generate such a ConstraintViolationException?Yautia
Y
5

In my opinion, the simplest way would be to mock your service into throwing the constraint violation in your test. You can do it manually by extending the class for example, or you can use a mocking framework such as mockito. I prefer mocking frameworks because they simplify things a lot as you neither have to create and maintain additional classes nor have to deal with injecting them in your objects under test.

Taking mockito as a starting point you'd probably write something similar to:

import org.hibernate.exception.ConstraintViolationException;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.Mockito.when;


public class MyTest {
    @Mock /* service mock */
    private MyService myService;

    @InjectMocks /* inject the mocks in the object under test */
    private ServiceCaller serviceCaller;

    @Test
    public void shouldHandleConstraintViolation() {
        // make the mock throw the exception when called
        when(myService.someMethod(...)).thenThrow(new ConstraintViolationException(...))

        // get the operation result
        MyResult result = serviceCaller.doSomeStuffWhichInvokesTheServiceMethodThrowingConstraintViolation();

        // verify all went according to plan
        assertWhatever(result);
    }
}
Yautia answered 8/4, 2014 at 15:14 Comment(5)
Thank you for the answer. I think I should have made it clear: this is not for testing purposes, it's production code.Globose
Then unless you can not alter the code that is catching the exception, I see no point in throwing a constraint violation. You're way better of throwing a dedicated exception which makes it easier to understand what you're trying to achieveYautia
What I'm trying to say is that I don't see many reasons to throw a NullPointerException if my number should be > 5 but in fact it is 1, if you catch my driftYautia
Thank you for the recommendation. I eventually decided to implement my own custom exception as you suggested. I'm still using the same ValidationMessages from hibernate validator, only I parse it myself. That way I can reuse the same error text from both exceptions.Globose
Great! You can post the above as an answer and accept it as the correct one after a few days, it does not necessarily have to be someone else's. CheersYautia
R
9

One more reason why I don't like Hibernate Validator that particular. They make it really hard to create a simple violation programmatically, when it should be dead simple. I do have test code where I need to create a violation to feed to my mocked subsystem.

Anyway, short of rolling your own implementation of a violation contraint - here is what I do to create a violation for a field:

private static final String MESSAGE_TEMPLATE = "{messageTemplate}";
private static final String MESSAGE = "message";

public static <T, A extends Annotation> ConstraintViolation<T> forField(
  final T rootBean, 
  final Class<T> clazz,
  final Class<A> annotationClazz,
  final Object leafBean, 
  final String field, 
  final Object offendingValue) {

  ConstraintViolation<T> violation = null;
  try {
    Field member = clazz.getDeclaredField(field);
    A annotation = member.getAnnotation(annotationClazz);
    ConstraintDescriptor<A> descriptor = new ConstraintDescriptorImpl<>(
      new ConstraintHelper(), 
      member, 
      annotation, 
      ElementType.FIELD);
    Path p = PathImpl.createPathFromString(field);
    violation = ConstraintViolationImpl.forBeanValidation(
      MESSAGE_TEMPLATE, 
      MESSAGE, 
      clazz, 
      rootBean, 
      leafBean,
      offendingValue, 
      p, 
      descriptor, 
      ElementType.FIELD);
  } catch (NoSuchFieldException ignore) {}
  return violation;

}

HTH

Resemblance answered 25/2, 2015 at 9:48 Comment(1)
ConstraintViolationImpl.forBeanValidation() will only work starting from Hibernate Validator 5.xOrthopterous
Y
5

In my opinion, the simplest way would be to mock your service into throwing the constraint violation in your test. You can do it manually by extending the class for example, or you can use a mocking framework such as mockito. I prefer mocking frameworks because they simplify things a lot as you neither have to create and maintain additional classes nor have to deal with injecting them in your objects under test.

Taking mockito as a starting point you'd probably write something similar to:

import org.hibernate.exception.ConstraintViolationException;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import static org.mockito.Mockito.when;


public class MyTest {
    @Mock /* service mock */
    private MyService myService;

    @InjectMocks /* inject the mocks in the object under test */
    private ServiceCaller serviceCaller;

    @Test
    public void shouldHandleConstraintViolation() {
        // make the mock throw the exception when called
        when(myService.someMethod(...)).thenThrow(new ConstraintViolationException(...))

        // get the operation result
        MyResult result = serviceCaller.doSomeStuffWhichInvokesTheServiceMethodThrowingConstraintViolation();

        // verify all went according to plan
        assertWhatever(result);
    }
}
Yautia answered 8/4, 2014 at 15:14 Comment(5)
Thank you for the answer. I think I should have made it clear: this is not for testing purposes, it's production code.Globose
Then unless you can not alter the code that is catching the exception, I see no point in throwing a constraint violation. You're way better of throwing a dedicated exception which makes it easier to understand what you're trying to achieveYautia
What I'm trying to say is that I don't see many reasons to throw a NullPointerException if my number should be > 5 but in fact it is 1, if you catch my driftYautia
Thank you for the recommendation. I eventually decided to implement my own custom exception as you suggested. I'm still using the same ValidationMessages from hibernate validator, only I parse it myself. That way I can reuse the same error text from both exceptions.Globose
Great! You can post the above as an answer and accept it as the correct one after a few days, it does not necessarily have to be someone else's. CheersYautia
S
5

A couple of things here:

  1. ConstraintViolation is an interface, so you could just implement your own version

  2. Hibernate Validator uses its own internal implementation of this interface - org.hibernate.validator.internal.engine.ConstraintViolationImpl. It is a public class, but since it is in an internal package you are not encouraged to use it directly. However, you might get an idea what is needed to implement ConstraintViolation.

Sorority answered 9/4, 2014 at 9:15 Comment(0)
C
4

Why not inject the Validator in your test and create an object triggering the validation errors you want?

Set<ConstraintViolation<T>> res = validator.validate(object);
Carman answered 10/1, 2017 at 13:45 Comment(0)
S
-2

I encountered this issue several times already and came up with this little solution. It is based on the validateValue method of the Validator and using a helper-validator in the test class, which is used to generate in a one liner a Set<ConstraintViolation<T>> object.

suppose you are testing a class that internally uses a Validator, mocked in our test, on an object "Data data" which has Bean Validation annotations, for example the field "sessionId" is annotated with @NotNull.

Your test class would then have at least these two members:

@Mock
private Validator validator;

private Validator violationCreatorValidator;

@Before
public void setup() {
     MockitoAnnotations.initMocks(this);
     violationsGenerator.Validation.buildDefaultValidatorFactory().getValidator();
}

And then in the test case:

when(validator.validate(data))
.thenReturn(violationsGenerator.validateValue(Data.class, "sessionId", null));

Thats it. No need to a implement the ConstraintViolation interface or call the monstrous ConstraintViolationImpl.forBeanValidation. Let the Validator do the job for you.

Shelburne answered 18/10, 2018 at 12:5 Comment(4)
Can you please explain how violationsGenerator is implemented?Tager
This answer is useless without ViolationsGenerator implementation.Vallie
Actually this works for me, you should provide a dependency with the implementation like hibernate-validator or spring-boot-starter-validation in my case, but this solution works perfectly, I don't get the negative votes.Ribose
The negative votes are clearly because he didn't include the code for the generator.Fortalice

© 2022 - 2024 — McMap. All rights reserved.