diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java index e9dd050e0cc..aebfcbfe9fe 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java @@ -61,6 +61,12 @@ public @interface AutoConfigureMockMvc { @PropertyMapping(skip = SkipPropertyMapping.ON_DEFAULT_VALUE) MockMvcPrint print() default MockMvcPrint.DEFAULT; + /** + * If {@link MvcResult} information should be printed only if the test fails. + * @return {@code true} if printing only occurs on failure + */ + boolean printOnlyOnFailure() default true; + /** * If a {@link WebClient} should be auto-configured when HtmlUnit is on the classpath. * Defaults to {@code true}. diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcPrintOnlyOnFailureTestExecutionListener.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcPrintOnlyOnFailureTestExecutionListener.java new file mode 100644 index 00000000000..f3035360caa --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcPrintOnlyOnFailureTestExecutionListener.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2016 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.test.autoconfigure.web.servlet; + +import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.DeferredLinesWriter; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * {@link TestExecutionListener} used to print MVC lines only on failure. + * + * @author Phillip Webb + */ +class MockMvcPrintOnlyOnFailureTestExecutionListener + extends AbstractTestExecutionListener { + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + if (testContext.getTestException() != null) { + DeferredLinesWriter writer = DeferredLinesWriter + .get(testContext.getApplicationContext()); + if (writer != null) { + writer.writeDeferredResult(); + } + } + + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index f8ff9f2b422..22574273bfc 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -17,16 +17,26 @@ package org.springframework.boot.test.autoconfigure.web.servlet; import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import javax.servlet.Filter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletContextInitializerBeans; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.PrintingResultHandler; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.util.Assert; @@ -50,6 +60,8 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi private MockMvcPrint print = MockMvcPrint.DEFAULT; + private boolean printOnlyOnFailure = true; + /** * Create a new {@link SpringBootMockMvcBuilderCustomizer} instance. * @param context the source application context @@ -71,13 +83,25 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi } private ResultHandler getPrintHandler() { + LinesWriter writer = getLinesWriter(); + if (writer == null) { + return null; + } + if (this.printOnlyOnFailure) { + writer = new DeferredLinesWriter(this.context, writer); + } + return new LinesWritingResultHandler(writer); + } + + private LinesWriter getLinesWriter() { if (this.print == MockMvcPrint.NONE) { return null; } if (this.print == MockMvcPrint.LOG_DEBUG) { - return MockMvcResultHandlers.log(); + return new LoggingLinesWriter(); } - return new SystemResultHandler(this.print); + return new SystemLinesWriter(this.print); + } private void addFilters(ConfigurableMockMvcBuilder builder) { @@ -129,49 +153,168 @@ public class SpringBootMockMvcBuilderCustomizer implements MockMvcBuilderCustomi return this.print; } - /** - * {@link PrintingResultHandler} to deal with {@code System.out} and - * {@code System.err} printing. The actual {@link PrintStream} used to write the - * response is obtained as late as possible in case an {@code OutputCaptureRule} is - * being used. - */ - private static class SystemResultHandler extends PrintingResultHandler { + public void setPrintOnlyOnFailure(boolean printOnlyOnFailure) { + this.printOnlyOnFailure = printOnlyOnFailure; + } - protected SystemResultHandler(MockMvcPrint print) { - super(new SystemResultValuePrinter(print)); + public boolean isPrintOnlyOnFailure() { + return this.printOnlyOnFailure; + } + + /** + * {@link ResultHandler} that prints {@link MvcResult} details to a given + * {@link LinesWriter}. + */ + private static class LinesWritingResultHandler implements ResultHandler { + + private final LinesWriter writer; + + LinesWritingResultHandler(LinesWriter writer) { + this.writer = writer; } - private static class SystemResultValuePrinter implements ResultValuePrinter { + @Override + public void handle(MvcResult result) throws Exception { + LinesPrintingResultHandler delegate = new LinesPrintingResultHandler(); + delegate.handle(result); + delegate.write(this.writer); + } - private final MockMvcPrint print; + private static class LinesPrintingResultHandler extends PrintingResultHandler { - SystemResultValuePrinter(MockMvcPrint print) { - this.print = print; + protected LinesPrintingResultHandler() { + super(new Printer()); } - @Override - public void printHeading(String heading) { - getWriter().println(); - getWriter().println(String.format("%s:", heading)); + public void write(LinesWriter writer) { + writer.write(((Printer) getPrinter()).getLines()); } - @Override - public void printValue(String label, Object value) { - if (value != null && value.getClass().isArray()) { - value = CollectionUtils.arrayToList(value); + private static class Printer implements ResultValuePrinter { + + private final List lines = new ArrayList(); + + @Override + public void printHeading(String heading) { + this.lines.add(""); + this.lines.add(String.format("%s:", heading)); } - getWriter().println(String.format("%17s = %s", label, value)); - } - private PrintStream getWriter() { - if (this.print == MockMvcPrint.SYSTEM_ERR) { - return System.err; + @Override + public void printValue(String label, Object value) { + if (value != null && value.getClass().isArray()) { + value = CollectionUtils.arrayToList(value); + } + this.lines.add(String.format("%17s = %s", label, value)); } - return System.out; + + public List getLines() { + return this.lines; + } + } } } + /** + * Strategy interface to write MVC result lines. + */ + interface LinesWriter { + + void write(List lines); + + } + + /** + * {@link LinesWriter} used to defer writing until errors are detected. + * @see MockMvcPrintOnlyOnFailureTestExecutionListener + */ + static class DeferredLinesWriter implements LinesWriter { + + private static final String BEAN_NAME = DeferredLinesWriter.class.getName(); + + private final LinesWriter delegate; + + private final List lines = new ArrayList(); + + DeferredLinesWriter(WebApplicationContext context, LinesWriter delegate) { + Assert.state(context instanceof ConfigurableApplicationContext, + "A ConfigurableApplicationContext is required for printOnlyOnFailure"); + ((ConfigurableApplicationContext) context).getBeanFactory() + .registerSingleton(BEAN_NAME, this); + this.delegate = delegate; + } + + @Override + public void write(List lines) { + this.lines.addAll(lines); + } + + public void writeDeferredResult() { + this.delegate.write(this.lines); + } + + public static DeferredLinesWriter get(ApplicationContext applicationContext) { + try { + return applicationContext.getBean(BEAN_NAME, DeferredLinesWriter.class); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + + } + + /** + * {@link LinesWriter} to output results to the log. + */ + private static class LoggingLinesWriter implements LinesWriter { + + private static final Log logger = LogFactory + .getLog("org.springframework.test.web.servlet.result"); + + @Override + public void write(List lines) { + if (logger.isDebugEnabled()) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + for (String line : lines) { + printWriter.println(line); + } + logger.debug("MvcResult details:\n" + stringWriter); + } + } + + } + + /** + * {@link LinesWriter} to output results to {@code System.out} or {@code System.err}. + */ + private static class SystemLinesWriter implements LinesWriter { + + private final MockMvcPrint print; + + SystemLinesWriter(MockMvcPrint print) { + this.print = print; + } + + @Override + public void write(List lines) { + PrintStream printStream = getPrintStream(); + for (String line : lines) { + printStream.println(line); + } + } + + private PrintStream getPrintStream() { + if (this.print == MockMvcPrint.SYSTEM_ERR) { + return System.err; + } + return System.out; + } + + } + } diff --git a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index df74dcfb171..722162a648a 100644 --- a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -82,4 +82,5 @@ org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomiz org.springframework.test.context.TestExecutionListener=\ org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener,\ org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener,\ +org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener,\ org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcSpringBootTestIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcSpringBootTestIntegrationTests.java index 57f2267b175..46997f6c8ba 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcSpringBootTestIntegrationTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcSpringBootTestIntegrationTests.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. */ @RunWith(SpringRunner.class) @SpringBootTest -@AutoConfigureMockMvc(print = MockMvcPrint.SYSTEM_ERR) +@AutoConfigureMockMvc(print = MockMvcPrint.SYSTEM_ERR, printOnlyOnFailure = false) @WithMockUser(username = "user", password = "secret") public class MockMvcSpringBootTestIntegrationTests { diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintAlwaysIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintAlwaysIntegrationTests.java new file mode 100644 index 00000000000..ac941077897 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintAlwaysIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2016 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.test.autoconfigure.web.servlet; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link WebMvcTest} default print output. + * + * @author Phillip Webb + */ +@RunWith(SpringRunner.class) +@WebMvcTest +@AutoConfigureMockMvc(secure = false, printOnlyOnFailure = false) +public class WebMvcTestPrintAlwaysIntegrationTests { + + @Rule + public OutputCapture output = new OutputCapture(); + + @Autowired + private MockMvc mvc; + + @Test + public void shouldPrint() throws Exception { + this.mvc.perform(get("/one")).andExpect(content().string("one")) + .andExpect(status().isOk()); + assertThat(this.output.toString()).contains("Request URI = /one"); + } + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultIntegrationTests.java index 89a90c2d76d..1cf44e68e7b 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultIntegrationTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultIntegrationTests.java @@ -16,16 +16,12 @@ package org.springframework.boot.test.autoconfigure.web.servlet; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.rule.OutputCapture; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -35,21 +31,24 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * * @author Phillip Webb */ -@RunWith(SpringRunner.class) -@WebMvcTest(secure = false) +@RunWith(WebMvcTestPrintDefaultRunner.class) +@WebMvcTest +@AutoConfigureMockMvc(secure = false) public class WebMvcTestPrintDefaultIntegrationTests { - @Rule - public OutputCapture output = new OutputCapture(); - @Autowired private MockMvc mvc; @Test - public void shouldPrint() throws Exception { + public void shouldNotPrint() throws Exception { this.mvc.perform(get("/one")).andExpect(content().string("one")) .andExpect(status().isOk()); - assertThat(this.output.toString()).contains("Request URI = /one"); + } + + @Test + public void shouldPrint() throws Exception { + this.mvc.perform(get("/one")).andExpect(content().string("none")) + .andExpect(status().isOk()); } } diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultRunner.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultRunner.java new file mode 100644 index 00000000000..07a6c5e33cd --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestPrintDefaultRunner.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2016 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.test.autoconfigure.web.servlet; + +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +/** + * Test runner used for {@link WebMvcTestPrintDefaultIntegrationTests}. + * + * @author Phillip Webb + */ +public class WebMvcTestPrintDefaultRunner extends SpringJUnit4ClassRunner { + + public WebMvcTestPrintDefaultRunner(Class clazz) throws InitializationError { + super(clazz); + } + + @Override + protected Statement methodBlock(FrameworkMethod frameworkMethod) { + Statement statement = super.methodBlock(frameworkMethod); + statement = new AlwaysPassStatement(statement); + OutputCapture outputCapture = new OutputCapture(); + if (frameworkMethod.getName().equals("shouldPrint")) { + outputCapture.expect(containsString("HTTP Method")); + } + else if (frameworkMethod.getName().equals("shouldNotPrint")) { + outputCapture.expect(not(containsString("HTTP Method"))); + } + else { + throw new IllegalStateException("Unexpected test method"); + } + System.err.println(frameworkMethod.getName()); + return outputCapture.apply(statement, null); + } + + private static class AlwaysPassStatement extends Statement { + + private final Statement delegate; + + AlwaysPassStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + try { + this.delegate.evaluate(); + } + catch (AssertionError ex) { + } + } + + } + +}