From ac2609b5857727724709e7da793d82264bd6726d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 17 Sep 2016 22:36:58 -0700 Subject: [PATCH] Fix WebDriver lifecycle issues Update WebDriver support to ensure that the `.quit()` method is called after each test method runs and that a new WebDriver instance is injected each time. Support is provided by introducing a new `Scope` which is applied by a ContextCustomizerFactory and reset by a TestExecutionListener. Fixes gh-6641 --- .../WebDriverContextCustomizerFactory.java | 70 +++++++++ .../web/servlet/WebDriverScope.java | 148 ++++++++++++++++++ .../WebDriverTestExecutionListener.java | 44 ++++++ .../main/resources/META-INF/spring.factories | 6 +- .../WebMvcTestWebDriverIntegrationTests.java | 24 +++ 5 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverTestExecutionListener.java diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java new file mode 100644 index 0000000000..4d47950250 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java @@ -0,0 +1,70 @@ +/* + * 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 java.util.List; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * {@link ContextCustomizerFactory} to register a {@link WebDriverScope} and configure + * appropriate bean definitions to use it. Expects the scope to be reset with a + * {@link WebDriverTestExecutionListener}. + * + * @author Phillip Webb + * @see WebDriverTestExecutionListener + * @see WebDriverScope + */ +class WebDriverContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + return new Customizer(); + } + + private static class Customizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedConfig) { + WebDriverScope.registerWith(context); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || !obj.getClass().equals(getClass())) { + return false; + } + return true; + } + + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java new file mode 100644 index 0000000000..e58b845aa8 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java @@ -0,0 +1,148 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import org.openqa.selenium.WebDriver; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * A special scope used for {@link WebDriver} beans. Usually registered by a + * {@link WebDriverContextCustomizerFactory} and reset by a + * {@link WebDriverTestExecutionListener}. + * + * @author Phillip Webb + * @see WebDriverContextCustomizerFactory + * @see WebDriverTestExecutionListener + */ +class WebDriverScope implements Scope { + + public static final String NAME = "webDriver"; + + private static final String WEB_DRIVER_CLASS = "org.openqa.selenium.WebDriver"; + + private static final String[] BEAN_CLASSES = { WEB_DRIVER_CLASS, + "org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder" }; + + private Map instances = new HashMap(); + + @Override + public Object get(String name, ObjectFactory objectFactory) { + synchronized (this.instances) { + Object instance = this.instances.get(name); + if (instance == null) { + instance = objectFactory.getObject(); + this.instances.put(name, instance); + } + return instance; + } + } + + @Override + public Object remove(String name) { + synchronized (this.instances) { + return this.instances.remove(name); + } + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return null; + } + + /** + * Reset all instances in the scope. + * @return {@code true} if items were reset + */ + public boolean reset() { + boolean reset = false; + synchronized (this.instances) { + for (Object instance : this.instances.values()) { + reset = true; + if (instance instanceof WebDriver) { + ((WebDriver) instance).quit(); + } + } + this.instances.clear(); + } + return reset; + } + + /** + * Register this scope with the specified context and reassign appropriate bean + * definitions to used it. + * @param context the application context + */ + public static void registerWith(ConfigurableApplicationContext context) { + if (!ClassUtils.isPresent(WEB_DRIVER_CLASS, null)) { + return; + } + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beanFactory.getRegisteredScope(NAME) == null) { + beanFactory.registerScope(NAME, new WebDriverScope()); + } + context.addBeanFactoryPostProcessor(new BeanFactoryPostProcessor() { + + @Override + public void postProcessBeanFactory( + ConfigurableListableBeanFactory beanFactory) throws BeansException { + for (String beanClass : BEAN_CLASSES) { + for (String beanName : beanFactory.getBeanNamesForType( + ClassUtils.resolveClassName(beanClass, null))) { + beanFactory.getBeanDefinition(beanName).setScope(NAME); + + } + } + } + + }); + } + + /** + * Return the {@link WebDriverScope} being used by the specified context (if any). + * @param context the application context + * @return the web driver scope or {@code null} + */ + public static WebDriverScope getFrom(ApplicationContext context) { + if (context instanceof ConfigurableApplicationContext) { + Scope scope = ((ConfigurableApplicationContext) context).getBeanFactory() + .getRegisteredScope(NAME); + return (scope instanceof WebDriverScope ? (WebDriverScope) scope : null); + } + return null; + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverTestExecutionListener.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverTestExecutionListener.java new file mode 100644 index 0000000000..1a50f516ee --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverTestExecutionListener.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.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * {@link TestExecutionListener} to reset the {@link WebDriverScope}. + * + * @author Phillip Webb + * @see WebDriverContextCustomizerFactory + * @see WebDriverScope + */ +class WebDriverTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + WebDriverScope scope = WebDriverScope + .getFrom(testContext.getApplicationContext()); + if (scope != null && scope.reset()) { + testContext.setAttribute( + DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, + Boolean.TRUE); + } + } + +} 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 ec9a19edb7..df74dcfb17 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 @@ -75,9 +75,11 @@ org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExe org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory,\ org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizerFactory,\ -org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory +org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory,\ +org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory # Test Execution Listeners 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.client.MockRestServiceServerResetTestExecutionListener,\ +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/WebMvcTestWebDriverIntegrationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestWebDriverIntegrationTests.java index 10f348ce7b..5004255ce3 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestWebDriverIntegrationTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestWebDriverIntegrationTests.java @@ -16,16 +16,21 @@ package org.springframework.boot.test.autoconfigure.web.servlet; +import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; /** * Tests for {@link WebMvcTest} with {@link WebDriver}. @@ -34,8 +39,11 @@ import static org.assertj.core.api.Assertions.assertThat; */ @RunWith(SpringRunner.class) @WebMvcTest(secure = false) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class WebMvcTestWebDriverIntegrationTests { + private static WebDriver previousWebDriver; + @Autowired private WebDriver webDriver; @@ -44,6 +52,22 @@ public class WebMvcTestWebDriverIntegrationTests { this.webDriver.get("/html"); WebElement element = this.webDriver.findElement(By.tagName("body")); assertThat(element.getText()).isEqualTo("Hello"); + WebMvcTestWebDriverIntegrationTests.previousWebDriver = this.webDriver; + } + + @Test + public void shouldBeADifferentWebClient() throws Exception { + this.webDriver.get("/html"); + WebElement element = this.webDriver.findElement(By.tagName("body")); + assertThat(element.getText()).isEqualTo("Hello"); + try { + ReflectionTestUtils.invokeMethod(previousWebDriver, "getCurrentWindow"); + fail("Did not call quit()"); + } + catch (NoSuchWindowException ex) { + // Expected + } + assertThat(previousWebDriver).isNotNull().isNotSameAs(this.webDriver); } }