5

I'm trying to test that my beans have correct validation annotations. I'm using spring-boot. Here is an example test case:

package com.example.sandbox;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.validation.annotation.Validated;

@SpringBootTest
class ValidationTest {

    @Test
    void testConstructor() {
        TestedBean bean = new TestedBean(null);

        assertThatThrownBy(() -> checkIfValidated(bean)).isInstanceOf(ConstraintViolationException.class);
    }

    @Test
    void testSetter() {
        TestedBean bean = new TestedBean(null);

        assertThatThrownBy(() -> bean.setSomeProperty(null)).isInstanceOf(ConstraintViolationException.class);
    }

    private void checkIfValidated(@Valid TestedBean bean) {

    }

    @Validated
    class TestedBean {
        @NotNull
        private String someProperty;

        public TestedBean(String someProperty) {
            super();
            this.someProperty = someProperty;
        }

        public String getSomeProperty() {
            return someProperty;
        }

        public void setSomeProperty(@NotNull String someProperty) {
            this.someProperty = someProperty;
        }
    }
}

I expect the call to checkIfvalidated() and to setSomeProperty(null) to raise a ConstraintViolationException, and the tests to pass, but they both fail with:

java.lang.AssertionError: 
Expecting code to raise a throwable.
    at com.example.sandbox.ValidationTest.test(ValidationTest.java:20)
    ...

My pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>com.example.springbootsandbox</artifactId>
    <version>0.0</version>
    <name>SpringBootSandbox</name>
    <description>Sandbox for Spring Boot</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.1.5.Final</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Why is there no ConstraintViolationException raised here? The bean property has a @NotNull annotation, the bean itself is @Validated and the method signature requires a @Valid bean.

Is there a simple way to have that exception raised in the context of my test class?

When I use validation annotations on method signatures for a service interface, everything works as expected. I don't understand where is the difference.

Service interface:

package com.example.sandbox;

import javax.validation.constraints.NotNull;

import org.springframework.validation.annotation.Validated;

@Validated
public interface IService {
    public void setValue(@NotNull String value);
}

Service implementation:

package com.example.sandbox;

import org.springframework.stereotype.Service;

@Service
public class SomeService implements IService {
    @Override
    public void setValue(String value) {
        // Do nothing
    }
}

Test case:

package com.example.sandbox;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import javax.validation.ConstraintViolationException;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SomeServiceTests {
    @Autowired
    IService service;

    @Test
    void testSetValue() {
        assertThatThrownBy(() -> service.setValue(null)).isInstanceOf(ConstraintViolationException.class);
    }
}

==> The test passes.


Working code according to the given answer:

The test class:

@SpringBootTest
class ValidationTest {
    @Autowired
    private Validator validator; // Using the default validator to test property annotations

    @Autowired
    private TestedBeanService service; // Using a service to test method annotations

    @Test
    void testPropertyAnnotations() {
        TestedBean bean = new TestedBean(null);
        Set<ConstraintViolation<TestedBean>> violations = validator.validate(bean);
        assertThat(violations).isNotEmpty();
    }

    @Test
    void testMethodAnnotations() {
        TestedBean bean = new TestedBean(null);
        assertThatThrownBy(() -> service.setBeanProperty(bean, null)).isInstanceOf(ConstraintViolationException.class);
    }
}

The tested bean:

@Validated
class TestedBean {
    @NotNull
    private String someProperty;

    public TestedBean(String someProperty) {
        super();
        this.someProperty = someProperty;
    }

    public String getSomeProperty() {
        return someProperty;
    }

    public void setSomeProperty(String someProperty) { // No more annotation on setter
        this.someProperty = someProperty;
    }
}

The service interface:

@Validated
public interface TestedBeanService {
    // method annotation on the interface method
    void setBeanProperty(TestedBean bean, @NotNull String someProperty);
}

The service implementation:

@Service
public class TestedBeanServiceImpl implements TestedBeanService {

    @Override
    public void setBeanProperty(TestedBean bean, String someProperty) {
        bean.setSomeProperty(someProperty);
    }
}
EricC-59
  • 119
  • 2
  • 9

1 Answers1

2

Why is there no ConstraintViolationException raised here? The bean property has a @NotNull annotation, the bean itself is @Validated and the method signature requires a @Valid bean.

Annotations by themselves do not mean anything, they should be processed in some way. In this case the @Validated annotation is processed by Spring for its beans. The test is not a Spring bean, so the framework does not look at the annotations related to valdidation, hence no exception.

Even if the test were a Spring Bean, the approach may not work out of the box. See this question for details.

Is there a simple way to have that exception raised in the context of my test class?

Take a look at this question

When I use validation annotations on method signatures for a service interface, everything works as expected. I don't understand where is the difference.

This happens because the service is a Spring bean, but test is not. When a method on the service is invoked, it gets intercepted by MethodValidationInterceptor, which is not the case for test

Denis Zavedeev
  • 7,627
  • 4
  • 32
  • 53
  • Ok. So my objects are proxied and the proxies deal with the validation. I found some limitations, but I think it will guide my code: - I use the validation annotations on properties on my beans, and I can validate them with a `Validator` - I don't manage to trigger the validation annotations on my beans' setters - I can throw a `ConstraintValidationException` on my service methods, if I use the validation annotations in its interface method. When using the object directly, it doesn't work. So, I'll use properties annotations for my beans and method annotations for my services. Thanx – EricC-59 Dec 24 '20 at 10:24