From 09c64d52e1d8773df87b126c5f8668fe0055eade Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2017 09:12:09 +0100 Subject: [PATCH] Improve ApplicationContextRunner diagnostics when context start fails Closes gh-10250 --- .../assertj/ApplicationContextAssert.java | 113 ++++++++++++----- .../ApplicationContextAssertTests.java | 118 ++++++++++-------- 2 files changed, 150 insertions(+), 81 deletions(-) diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java index 35bf1d30d41..942d747f68d 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java @@ -16,6 +16,12 @@ package org.springframework.boot.test.context.assertj; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; + import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AbstractObjectArrayAssert; import org.assertj.core.api.AbstractObjectAssert; @@ -37,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @param The application context type * @author Phillip Webb + * @author Andy Wilkinson * @since 2.0.0 * @see ApplicationContextRunner * @see AssertableApplicationContext @@ -70,9 +77,8 @@ public class ApplicationContextAssert */ public ApplicationContextAssert hasBean(String name) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto have bean named:%n <%s>%nbut context failed to start", - getApplicationContext(), name)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to have bean named:%n <%s>", name)); } if (findBean(name) == null) { throwAssertionError(new BasicErrorMessageFactory( @@ -96,9 +102,8 @@ public class ApplicationContextAssert */ public ApplicationContextAssert hasSingleBean(Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto have a single bean of type:%n <%s>%nbut context failed to start", - getApplicationContext(), type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to have a single bean of type:%n <%s>", type)); } String[] names = getApplicationContext().getBeanNamesForType(type); if (names.length == 0) { @@ -127,9 +132,8 @@ public class ApplicationContextAssert */ public ApplicationContextAssert doesNotHaveBean(Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nnot to have any beans of type:%n <%s>%nbut context failed to start", - getApplicationContext(), type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "not to have any beans of type:%n <%s>", type)); } String[] names = getApplicationContext().getBeanNamesForType(type); if (names.length > 0) { @@ -153,9 +157,8 @@ public class ApplicationContextAssert */ public ApplicationContextAssert doesNotHaveBean(String name) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nnot to have any beans of name:%n <%s>%nbut context failed to start", - getApplicationContext(), name)); + throwAssertionError(contextFailedToStartWhenExpecting( + "not to have any beans of name:%n <%s>", name)); } try { Object bean = getApplicationContext().getBean(name); @@ -181,9 +184,8 @@ public class ApplicationContextAssert */ public AbstractObjectArrayAssert getBeanNames(Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto get beans names with type:%n <%s>%nbut context failed to start", - getApplicationContext(), type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to get beans names with type:%n <%s>", type)); } return Assertions.assertThat(getApplicationContext().getBeanNamesForType(type)) .as("Bean names of type <%s> from <%s>", type, getApplicationContext()); @@ -207,9 +209,8 @@ public class ApplicationContextAssert */ public AbstractObjectAssert getBean(Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto contain bean of type:%n <%s>%nbut context failed to start", - getApplicationContext(), type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to contain bean of type:%n <%s>", type)); } String[] names = getApplicationContext().getBeanNamesForType(type); if (names.length > 1) { @@ -238,9 +239,8 @@ public class ApplicationContextAssert */ public AbstractObjectAssert getBean(String name) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto contain a bean of name:%n <%s>%nbut context failed to start", - getApplicationContext(), name)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to contain a bean of name:%n <%s>", name)); } Object bean = findBean(name); return Assertions.assertThat(bean).as("Bean of name <%s> from <%s>", name, @@ -267,9 +267,8 @@ public class ApplicationContextAssert @SuppressWarnings("unchecked") public AbstractObjectAssert getBean(String name, Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto contain a bean of name:%n <%s> (%s)%nbut context failed to start", - getApplicationContext(), name, type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to contain a bean of name:%n <%s> (%s)", name, type)); } Object bean = findBean(name); if (bean != null && type != null && !type.isInstance(bean)) { @@ -307,9 +306,8 @@ public class ApplicationContextAssert */ public MapAssert getBeans(Class type) { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto get beans of type:%n <%s> (%s)%nbut context failed to start", - getApplicationContext(), type, type)); + throwAssertionError(contextFailedToStartWhenExpecting( + "to get beans of type:%n <%s>", type)); } return Assertions.assertThat(getApplicationContext().getBeansOfType(type)) .as("Beans of type <%s> from <%s>", type, getApplicationContext()); @@ -357,9 +355,7 @@ public class ApplicationContextAssert */ public ApplicationContextAssert hasNotFailed() { if (this.startupFailure != null) { - throwAssertionError(new BasicErrorMessageFactory( - "%nExpecting:%n <%s>%nto have not failed:%nbut context failed to start", - getApplicationContext())); + throwAssertionError(contextFailedToStartWhenExpecting("to have not failed")); } return this; } @@ -372,4 +368,61 @@ public class ApplicationContextAssert return this.startupFailure; } + private ContextFailedToStart contextFailedToStartWhenExpecting( + String expectationFormat, Object... arguments) { + return new ContextFailedToStart(getApplicationContext(), this.startupFailure, + expectationFormat, arguments); + } + + private static final class ContextFailedToStart + extends BasicErrorMessageFactory { + + private ContextFailedToStart(C context, Throwable ex, String expectationFormat, + Object... arguments) { + super("%nExpecting:%n <%s>%n" + expectationFormat + + ":%nbut context failed to start:%n%s", + combineArguments(context.toString(), ex, arguments)); + } + + private static Object[] combineArguments(String context, Throwable ex, + Object[] arguments) { + Object[] combinedArguments = new Object[arguments.length + 2]; + combinedArguments[0] = unquotedString(context); + System.arraycopy(arguments, 0, combinedArguments, 1, arguments.length); + combinedArguments[combinedArguments.length - 1] = unquotedString( + getIndentedStackTraceAsString(ex)); + return combinedArguments; + } + + private static String getIndentedStackTraceAsString(Throwable ex) { + String stackTrace = getStackTraceAsString(ex); + return indent(stackTrace); + } + + private static String getStackTraceAsString(Throwable ex) { + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + ex.printStackTrace(printer); + return writer.toString(); + } + + private static String indent(String input) { + BufferedReader reader = new BufferedReader(new StringReader(input)); + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + try { + String line; + while ((line = reader.readLine()) != null) { + printer.print(" "); + printer.println(line); + } + return writer.toString(); + } + catch (IOException ex) { + return input; + } + } + + } + } diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertTests.java index b9767bbd06c..7cf22c871f3 100644 --- a/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertTests.java +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertTests.java @@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link ApplicationContextAssert}. * * @author Phillip Webb + * @uathor Andy Wilkinson */ public class ApplicationContextAssertTests { @@ -40,59 +41,60 @@ public class ApplicationContextAssertTests { private RuntimeException failure = new RuntimeException(); @Test - public void createWhenApplicationContextIsNullShouldThrowException() - throws Exception { + public void createWhenApplicationContextIsNullShouldThrowException() { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("ApplicationContext must not be null"); new ApplicationContextAssert<>(null, null); } @Test - public void createWhenHasApplicationContextShouldSetActual() throws Exception { + public void createWhenHasApplicationContextShouldSetActual() { assertThat(getAssert(this.context).getSourceApplicationContext()) .isSameAs(this.context); } @Test - public void createWhenHasExceptionShouldSetFailure() throws Exception { + public void createWhenHasExceptionShouldSetFailure() { assertThat(getAssert(this.failure)).getFailure().isSameAs(this.failure); } @Test - public void hasBeanWhenHasBeanShouldPass() throws Exception { + public void hasBeanWhenHasBeanShouldPass() { this.context.registerSingleton("foo", Foo.class); assertThat(getAssert(this.context)).hasBean("foo"); } @Test - public void hasBeanWhenHasNoBeanShouldFail() throws Exception { + public void hasBeanWhenHasNoBeanShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("no such bean"); assertThat(getAssert(this.context)).hasBean("foo"); } @Test - public void hasBeanWhenNotStartedShouldFail() throws Exception { + public void hasBeanWhenNotStartedShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).hasBean("foo"); } @Test - public void hasSingleBeanWhenHasSingleBeanShouldPass() throws Exception { + public void hasSingleBeanWhenHasSingleBeanShouldPass() { this.context.registerSingleton("foo", Foo.class); assertThat(getAssert(this.context)).hasSingleBean(Foo.class); } @Test - public void hasSingleBeanWhenHasNoBeansShouldFail() throws Exception { + public void hasSingleBeanWhenHasNoBeansShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("no beans of that type"); + this.thrown.expectMessage("to have a single bean of type"); assertThat(getAssert(this.context)).hasSingleBean(Foo.class); } @Test - public void hasSingleBeanWhenHasMultipleShouldFail() throws Exception { + public void hasSingleBeanWhenHasMultipleShouldFail() { this.context.registerSingleton("foo", Foo.class); this.context.registerSingleton("bar", Foo.class); this.thrown.expect(AssertionError.class); @@ -101,19 +103,21 @@ public class ApplicationContextAssertTests { } @Test - public void hasSingleBeanWhenFailedToStartShouldFail() throws Exception { + public void hasSingleBeanWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("to have a single bean of type"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).hasSingleBean(Foo.class); } @Test - public void doesNotHaveBeanOfTypeWhenHasNoBeanOfTypeShouldPass() throws Exception { + public void doesNotHaveBeanOfTypeWhenHasNoBeanOfTypeShouldPass() { assertThat(getAssert(this.context)).doesNotHaveBean(Foo.class); } @Test - public void doesNotHaveBeanOfTypeWhenHasBeanOfTypeShouldFail() throws Exception { + public void doesNotHaveBeanOfTypeWhenHasBeanOfTypeShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("but found"); this.context.registerSingleton("foo", Foo.class); @@ -121,19 +125,21 @@ public class ApplicationContextAssertTests { } @Test - public void doesNotHaveBeanOfTypeWhenFailedToStartShouldFail() throws Exception { + public void doesNotHaveBeanOfTypeWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("not to have any beans of type"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).doesNotHaveBean(Foo.class); } @Test - public void doesNotHaveBeanOfNameWhenHasNoBeanOfTypeShouldPass() throws Exception { + public void doesNotHaveBeanOfNameWhenHasNoBeanOfTypeShouldPass() { assertThat(getAssert(this.context)).doesNotHaveBean("foo"); } @Test - public void doesNotHaveBeanOfNameWhenHasBeanOfTypeShouldFail() throws Exception { + public void doesNotHaveBeanOfNameWhenHasBeanOfTypeShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("but found"); this.context.registerSingleton("foo", Foo.class); @@ -141,14 +147,15 @@ public class ApplicationContextAssertTests { } @Test - public void doesNotHaveBeanOfNameWhenFailedToStartShouldFail() throws Exception { + public void doesNotHaveBeanOfNameWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); + this.thrown.expectMessage("not to have any beans of name"); this.thrown.expectMessage("failed to start"); assertThat(getAssert(this.failure)).doesNotHaveBean("foo"); } @Test - public void getBeanNamesWhenHasNamesShouldReturnNamesAssert() throws Exception { + public void getBeanNamesWhenHasNamesShouldReturnNamesAssert() { this.context.registerSingleton("foo", Foo.class); this.context.registerSingleton("bar", Foo.class); assertThat(getAssert(this.context)).getBeanNames(Foo.class).containsOnly("foo", @@ -156,30 +163,32 @@ public class ApplicationContextAssertTests { } @Test - public void getBeanNamesWhenHasNoNamesShouldReturnEmptyAssert() throws Exception { + public void getBeanNamesWhenHasNoNamesShouldReturnEmptyAssert() { assertThat(getAssert(this.context)).getBeanNames(Foo.class).isEmpty(); } @Test - public void getBeanNamesWhenFailedToStartShouldFail() throws Exception { + public void getBeanNamesWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("not to have any beans of name"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).doesNotHaveBean("foo"); } @Test - public void getBeanOfTypeWhenHasBeanShouldReturnBeanAssert() throws Exception { + public void getBeanOfTypeWhenHasBeanShouldReturnBeanAssert() { this.context.registerSingleton("foo", Foo.class); assertThat(getAssert(this.context)).getBean(Foo.class).isNotNull(); } @Test - public void getBeanOfTypeWhenHasNoBeanShouldReturnNullAssert() throws Exception { + public void getBeanOfTypeWhenHasNoBeanShouldReturnNullAssert() { assertThat(getAssert(this.context)).getBean(Foo.class).isNull(); } @Test - public void getBeanOfTypeWhenHasMultipleBeansShouldFail() throws Exception { + public void getBeanOfTypeWhenHasMultipleBeansShouldFail() { this.context.registerSingleton("foo", Foo.class); this.context.registerSingleton("bar", Foo.class); this.thrown.expect(AssertionError.class); @@ -188,46 +197,47 @@ public class ApplicationContextAssertTests { } @Test - public void getBeanOfTypeWhenFailedToStartShouldFail() throws Exception { + public void getBeanOfTypeWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("to contain bean of type"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).getBean(Foo.class); } @Test - public void getBeanOfNameWhenHasBeanShouldReturnBeanAssert() throws Exception { + public void getBeanOfNameWhenHasBeanShouldReturnBeanAssert() { this.context.registerSingleton("foo", Foo.class); assertThat(getAssert(this.context)).getBean("foo").isNotNull(); } @Test - public void getBeanOfNameWhenHasNoBeanOfNameShouldReturnNullAssert() - throws Exception { + public void getBeanOfNameWhenHasNoBeanOfNameShouldReturnNullAssert() { assertThat(getAssert(this.context)).getBean("foo").isNull(); } @Test - public void getBeanOfNameWhenFailedToStartShouldFail() throws Exception { + public void getBeanOfNameWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("to contain a bean of name"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).getBean("foo"); } @Test - public void getBeanOfNameAndTypeWhenHasBeanShouldReturnBeanAssert() throws Exception { + public void getBeanOfNameAndTypeWhenHasBeanShouldReturnBeanAssert() { this.context.registerSingleton("foo", Foo.class); assertThat(getAssert(this.context)).getBean("foo", Foo.class).isNotNull(); } @Test - public void getBeanOfNameAndTypeWhenHasNoBeanOfNameShouldReturnNullAssert() - throws Exception { + public void getBeanOfNameAndTypeWhenHasNoBeanOfNameShouldReturnNullAssert() { assertThat(getAssert(this.context)).getBean("foo", Foo.class).isNull(); } @Test - public void getBeanOfNameAndTypeWhenHasNoBeanOfNameButDifferentTypeShouldFail() - throws Exception { + public void getBeanOfNameAndTypeWhenHasNoBeanOfNameButDifferentTypeShouldFail() { this.context.registerSingleton("foo", Foo.class); this.thrown.expect(AssertionError.class); this.thrown.expectMessage("of type"); @@ -235,14 +245,16 @@ public class ApplicationContextAssertTests { } @Test - public void getBeanOfNameAndTypeWhenFailedToStartShouldFail() throws Exception { + public void getBeanOfNameAndTypeWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("to contain a bean of name"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).getBean("foo", Foo.class); } @Test - public void getBeansWhenHasBeansShouldReturnMapAssert() throws Exception { + public void getBeansWhenHasBeansShouldReturnMapAssert() { this.context.registerSingleton("foo", Foo.class); this.context.registerSingleton("bar", Foo.class); assertThat(getAssert(this.context)).getBeans(Foo.class).hasSize(2) @@ -250,50 +262,54 @@ public class ApplicationContextAssertTests { } @Test - public void getBeansWhenHasNoBeansShouldReturnEmptyMapAssert() throws Exception { + public void getBeansWhenHasNoBeansShouldReturnEmptyMapAssert() { assertThat(getAssert(this.context)).getBeans(Foo.class).isEmpty(); } @Test - public void getBeansWhenFailedToStartShouldFail() throws Exception { + public void getBeansWhenFailedToStartShouldFail() { this.thrown.expect(AssertionError.class); - this.thrown.expectMessage("failed to start"); + this.thrown.expectMessage("to get beans of type"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).getBeans(Foo.class); } @Test - public void getFailureWhenFailedShouldReturnFailure() throws Exception { + public void getFailureWhenFailedShouldReturnFailure() { assertThat(getAssert(this.failure)).getFailure().isSameAs(this.failure); } @Test - public void getFailureWhenDidNotFailShouldFail() throws Exception { + public void getFailureWhenDidNotFailShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("context started"); assertThat(getAssert(this.context)).getFailure(); } @Test - public void hasFailedWhenFailedShouldPass() throws Exception { + public void hasFailedWhenFailedShouldPass() { assertThat(getAssert(this.failure)).hasFailed(); } @Test - public void hasFailedWhenNotFailedShouldFail() throws Exception { + public void hasFailedWhenNotFailedShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("to have failed"); assertThat(getAssert(this.context)).hasFailed(); } @Test - public void hasNotFailedWhenFailedShouldFail() throws Exception { + public void hasNotFailedWhenFailedShouldFail() { this.thrown.expect(AssertionError.class); this.thrown.expectMessage("to have not failed"); + this.thrown.expectMessage(String + .format("but context failed to start:%n java.lang.RuntimeException")); assertThat(getAssert(this.failure)).hasNotFailed(); } @Test - public void hasNotFailedWhenNotFailedShouldPass() throws Exception { + public void hasNotFailedWhenNotFailedShouldPass() { assertThat(getAssert(this.context)).hasNotFailed(); }