/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.diagnostics.analyzer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link NoSuchBeanDefinitionFailureAnalyzer}.
*
* @author Stephane Nicoll
*/
public class NoSuchBeanDefinitionFailureAnalyzerTests {
private final NoSuchBeanDefinitionFailureAnalyzer analyzer = new NoSuchBeanDefinitionFailureAnalyzer();
@Test
public void failureAnalysisForMultipleBeans() {
FailureAnalysis analysis = analyzeFailure(
new NoUniqueBeanDefinitionException(String.class, 2, "Test"));
assertThat(analysis).isNull();
}
@Test
public void failureAnalysisForNoMatchType() {
FailureAnalysis analysis = analyzeFailure(createFailure(StringHandler.class));
assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
String.class);
assertThat(analysis.getDescription()).doesNotContain(
"No matching auto-configuration has been found for this type.");
assertThat(analysis.getAction()).startsWith(String.format(
"Consider defining a bean of type '%s' in your configuration.",
String.class.getName()));
}
@Test
public void failureAnalysisForMissingPropertyExactType() {
FailureAnalysis analysis = analyzeFailure(
createFailure(StringPropertyTypeConfiguration.class));
assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
String.class);
assertBeanMethodDisabled(analysis,
"did not find property 'spring.string.enabled'",
TestPropertyAutoConfiguration.class, "string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForMissingCollectionType() throws Exception {
FailureAnalysis analysis = analyzeFailure(
createFailure(StringCollectionConfiguration.class));
assertDescriptionConstructorMissingType(analysis, StringCollectionHandler.class,
0, String.class);
assertBeanMethodDisabled(analysis,
"did not find property 'spring.string.enabled'",
TestPropertyAutoConfiguration.class, "string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForMissingMapType() throws Exception {
FailureAnalysis analysis = analyzeFailure(
createFailure(StringMapConfiguration.class));
assertDescriptionConstructorMissingType(analysis, StringMapHandler.class, 0,
String.class);
assertBeanMethodDisabled(analysis,
"did not find property 'spring.string.enabled'",
TestPropertyAutoConfiguration.class, "string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForMissingPropertySubType() {
FailureAnalysis analysis = analyzeFailure(
createFailure(IntegerPropertyTypeConfiguration.class));
assertThat(analysis).isNotNull();
assertDescriptionConstructorMissingType(analysis, NumberHandler.class, 0,
Number.class);
assertBeanMethodDisabled(analysis,
"did not find property 'spring.integer.enabled'",
TestPropertyAutoConfiguration.class, "integer");
assertActionMissingType(analysis, Number.class);
}
@Test
public void failureAnalysisForMissingClassOnAutoConfigurationType() {
FailureAnalysis analysis = analyzeFailure(
createFailure(MissingClassOnAutoConfigurationConfiguration.class));
assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
String.class);
assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'",
"string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForExcludedAutoConfigurationType() {
FatalBeanException failure = createFailure(StringHandler.class);
addExclusions(this.analyzer, TestPropertyAutoConfiguration.class);
FailureAnalysis analysis = analyzeFailure(failure);
assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
String.class);
String configClass = ClassUtils
.getShortName(TestPropertyAutoConfiguration.class.getName());
assertClassDisabled(analysis,
String.format("auto-configuration '%s' was excluded", configClass),
"string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForSeveralConditionsType() {
FailureAnalysis analysis = analyzeFailure(
createFailure(SeveralAutoConfigurationTypeConfiguration.class));
assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0,
String.class);
assertBeanMethodDisabled(analysis,
"did not find property 'spring.string.enabled'",
TestPropertyAutoConfiguration.class, "string");
assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'",
"string");
assertActionMissingType(analysis, String.class);
}
@Test
public void failureAnalysisForNoMatchName() {
FailureAnalysis analysis = analyzeFailure(createFailure(StringNameHandler.class));
assertThat(analysis.getDescription()).startsWith(String.format(
"Constructor in %s required a bean named '%s' that could not be found",
StringNameHandler.class.getName(), "test-string"));
assertThat(analysis.getDescription().contains(
"No matching auto-configuration has been found for this bean name."));
assertThat(analysis.getAction()).startsWith(String.format(
"Consider defining a bean named '%s' in your configuration.",
"test-string"));
}
@Test
public void failureAnalysisForMissingBeanName() {
FailureAnalysis analysis = analyzeFailure(
createFailure(StringMissingBeanNameConfiguration.class));
assertThat(analysis.getDescription()).startsWith(String.format(
"Constructor in %s required a bean named '%s' that could not be found",
StringNameHandler.class.getName(), "test-string"));
assertBeanMethodDisabled(analysis,
"@ConditionalOnBean (types: java.lang.Integer; SearchStrategy: all) did not find any beans",
TestMissingBeanAutoConfiguration.class, "string");
assertActionMissingName(analysis, "test-string");
}
private void assertDescriptionConstructorMissingType(FailureAnalysis analysis,
Class<?> component, int index, Class<?> type) {
String expected = String.format(
"Parameter %s of constructor in %s required a bean of "
+ "type '%s' that could not be found.",
index, component.getName(), type.getName());
assertThat(analysis.getDescription()).startsWith(expected);
}
private void assertActionMissingType(FailureAnalysis analysis, Class<?> type) {
assertThat(analysis.getAction()).startsWith(String
.format("Consider revisiting the conditions above or defining a bean of type '%s' "
+ "in your configuration.", type.getName()));
}
private void assertActionMissingName(FailureAnalysis analysis, String name) {
assertThat(analysis.getAction()).startsWith(String
.format("Consider revisiting the conditions above or defining a bean named '%s' "
+ "in your configuration.", name));
}
private void assertBeanMethodDisabled(FailureAnalysis analysis, String description,
Class<?> target, String methodName) {
String expected = String.format("Bean method '%s' in '%s' not loaded because",
methodName, ClassUtils.getShortName(target));
assertThat(analysis.getDescription()).contains(expected);
assertThat(analysis.getDescription()).contains(description);
}
private void assertClassDisabled(FailureAnalysis analysis, String description,
String methodName) {
String expected = String.format("Bean method '%s' not loaded because",
methodName);
assertThat(analysis.getDescription()).contains(expected);
assertThat(analysis.getDescription()).contains(description);
}
private static void addExclusions(NoSuchBeanDefinitionFailureAnalyzer analyzer,
Class<?>... classes) {
ConditionEvaluationReport report = (ConditionEvaluationReport) new DirectFieldAccessor(
analyzer).getPropertyValue("report");
List<String> exclusions = new ArrayList<>(report.getExclusions());
for (Class<?> c : classes) {
exclusions.add(c.getName());
}
report.recordExclusions(exclusions);
}
private FatalBeanException createFailure(Class<?> config, String... environment) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
this.analyzer.setBeanFactory(context.getBeanFactory());
EnvironmentTestUtils.addEnvironment(context, environment);
context.register(config);
try {
context.refresh();
return null;
}
catch (FatalBeanException ex) {
return ex;
}
finally {
context.close();
}
}
private FailureAnalysis analyzeFailure(Exception failure) {
FailureAnalysis analysis = this.analyzer.analyze(failure);
if (analysis != null) {
new LoggingFailureAnalysisReporter().report(analysis);
}
return analysis;
}
@Configuration
@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
@Import(StringHandler.class)
protected static class StringPropertyTypeConfiguration {
}
@Configuration
@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
@Import(StringCollectionHandler.class)
protected static class StringCollectionConfiguration {
}
@Configuration
@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
@Import(StringMapHandler.class)
protected static class StringMapConfiguration {
}
@Configuration
@ImportAutoConfiguration(TestPropertyAutoConfiguration.class)
@Import(NumberHandler.class)
protected static class IntegerPropertyTypeConfiguration {
}
@Configuration
@ImportAutoConfiguration(TestTypeClassAutoConfiguration.class)
@Import(StringHandler.class)
protected static class MissingClassOnAutoConfigurationConfiguration {
}
@Configuration
@ImportAutoConfiguration({ TestPropertyAutoConfiguration.class,
TestTypeClassAutoConfiguration.class })
@Import(StringHandler.class)
protected static class SeveralAutoConfigurationTypeConfiguration {
}
@Configuration
@ImportAutoConfiguration(TestMissingBeanAutoConfiguration.class)
@Import(StringNameHandler.class)
protected static class StringMissingBeanNameConfiguration {
}
@Configuration
public static class TestPropertyAutoConfiguration {
@ConditionalOnProperty("spring.string.enabled")
@Bean
public String string() {
return "Test";
}
@ConditionalOnProperty("spring.integer.enabled")
@Bean
public Integer integer() {
return 42;
}
}
@Configuration
@ConditionalOnClass(name = "com.example.FooBar")
public static class TestTypeClassAutoConfiguration {
@Bean
public String string() {
return "Test";
}
}
@Configuration
public static class TestMissingBeanAutoConfiguration {
@ConditionalOnBean(Integer.class)
@Bean(name = "test-string")
public String string() {
return "Test";
}
}
protected static class StringHandler {
public StringHandler(String foo) {
}
}
protected static class NumberHandler {
public NumberHandler(Number foo) {
}
}
protected static class StringNameHandler {
public StringNameHandler(BeanFactory beanFactory) {
beanFactory.getBean("test-string");
}
}
protected static class StringCollectionHandler {
public StringCollectionHandler(Collection<String> collection) {
}
}
protected static class StringMapHandler {
public StringMapHandler(Map<String, String> map) {
}
}
}