5

I want to write a test for a @NotNull, @NotEmpty validation of @ConfigurationProperties.

@Configuration
@ConfigurationProperties(prefix = "myPrefix", ignoreUnknownFields = true)
@Getter
@Setter
@Validated
public class MyServerConfiguration {
  @NotNull
  @NotEmpty
  private String baseUrl;
}

My Test looks like this:

@RunWith(SpringRunner.class)
@SpringBootTest()
public class NoActiveProfileTest {
  @Test(expected = org.springframework.boot.context.properties.bind.validation.BindValidationException.class)
  public void should_ThrowException_IfMandatoryPropertyIsMissing() throws Exception {
  }

}

When I run the test, it reports a failure to start the application before the test is run:

***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target   org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'myPrefix' to com.xxxxx.configuration.MyServerConfiguration$$EnhancerBySpringCGLIB$$4b91954c failed:

How can I expect an Exception to write a negative test? Even if I replace the BindException.class with Throwable.class the application fails to start.

Derrick
  • 3,669
  • 5
  • 35
  • 50
TanjaMUC
  • 61
  • 4

2 Answers2

3

I'd use an ApplicationContextRunner for this, e.g.

new ApplicationContextRunner()
    .withUserConfiguration(MyServerConfiguration.class)
    .withPropertyValues("foo=bar")
    .run(context -> {
        var error = assertThrows(IllegalStateException.class, () -> context.getBean(MyServerConfiguration.class));

        var validationError = (BindValidationException) ExceptionUtils.getRootCause(error);
        var fieldViolation = (FieldError) validationError.getValidationErrors().iterator().next();
        var fieldInError = fieldViolation.getObjectName() + "." + fieldViolation.getField();

        assertThat(fieldInError, is(expectedFieldInError));
        assertThat(fieldViolation.getDefaultMessage(), is(expectedMessage));
    });
Miloš Milivojević
  • 5,219
  • 3
  • 26
  • 39
  • I didn't have to explicitly include `ConfigurationPropertiesAutoConfiguration` and `ValidationAutoConfiguration` with SpringBoot 2.7.1 - the validation still happens. Don't know if it matters, but I have 'org.springframework.boot:spring-boot-starter-validation' on the classpath. – jannis Nov 29 '22 at 22:20
  • @jannis You're most likely running a `@SpringBootTest` and not an ApplicationContextRunner-based test – Miloš Milivojević Nov 30 '22 at 10:50
  • Nope. It's a plain'ol Junit Jupiter test. I'm creating the runner this way: `new ApplicationContextRunner().withUserConfiguration(TestConfig.class)`. TestConfig has only a `@Configuration` and `@EnableConfigurationProperties(MyPropertiesUnderTest.class)` on it. – jannis Nov 30 '22 at 12:43
  • @jannis That is very weird then, I can't imagine it being the desired behaviour, the whole point of the ACR is to let you specify precicely which autoconfigurations to apply – Miloš Milivojević Nov 30 '22 at 19:55
  • 1
    See for yourself: https://github.com/jannis-baratheon/stackoverflow--spring-boot-bean-validation-test – jannis Dec 01 '22 at 09:42
  • 1
    @jannis Never said I doubted it happened, just questioned if it was behaving as desired if it picked up autoconfigs from the validation starter but turns out that wasn't the case - it's the binder itself that detects a JSR 303 validator on the classpath, will update the answer – Miloš Milivojević Dec 01 '22 at 16:13
  • @jannis it's crucial to have dependency `org.springframework.boot:spring-boot-starter-validation` on the classpath. Without it you can recieve following exception: `Unable to create a Configuration, because no Jakarta Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.` For me following dependency is enought: `testImplementation 'org.springframework.boot:spring-boot-starter-validation'` – Piotr Olaszewski Jan 16 '23 at 13:24
-1

Try to load Spring Boot Application context programmatically:

Simple version

public class AppFailIT {

    @Test
    public void testFail() {
        try {
            new AnnotationConfigServletWebServerApplicationContext(MyApplication.class);
        }
        catch (Exception e){
            Assertions.assertThat(e).isInstanceOf(UnsatisfiedDependencyException.class);
            Assertions.assertThat(e.getMessage()).contains("nested exception is org.springframework.boot.context.properties.bind.BindException: Failed to bind properties");
            return;
        }
        fail();
    }
}

Extended version with ability to load environment from application-test.properties and add own key:values to test environment on method level:

@TestPropertySource("classpath:application-test.properties")
public class AppFailIT {

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Autowired
    private ConfigurableEnvironment configurableEnvironment;

    @Test
    public void testFail() {
        try {
            MockEnvironment mockEnvironment = new MockEnvironment();
            mockEnvironment.withProperty("a","b");
            configurableEnvironment.merge(mockEnvironment);
            AnnotationConfigServletWebServerApplicationContext applicationContext = new AnnotationConfigServletWebServerApplicationContext();
            applicationContext.setEnvironment(configurableEnvironment);
            applicationContext.register(MyApplication.class);
            applicationContext.refresh();
        }
        catch (Exception e){
            Assertions.assertThat(e.getMessage()).contains("nested exception is org.springframework.boot.context.properties.bind.BindException: Failed to bind properties");
            return;
        }
        fail();
    }
}
qwazer
  • 7,174
  • 7
  • 44
  • 69