Skip to content

Commit ac2609b

Browse files
committed
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
1 parent 0ef845b commit ac2609b

File tree

5 files changed

+290
-2
lines changed

5 files changed

+290
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.servlet;
18+
19+
import java.util.List;
20+
21+
import org.springframework.context.ConfigurableApplicationContext;
22+
import org.springframework.test.context.ContextConfigurationAttributes;
23+
import org.springframework.test.context.ContextCustomizer;
24+
import org.springframework.test.context.ContextCustomizerFactory;
25+
import org.springframework.test.context.MergedContextConfiguration;
26+
27+
/**
28+
* {@link ContextCustomizerFactory} to register a {@link WebDriverScope} and configure
29+
* appropriate bean definitions to use it. Expects the scope to be reset with a
30+
* {@link WebDriverTestExecutionListener}.
31+
*
32+
* @author Phillip Webb
33+
* @see WebDriverTestExecutionListener
34+
* @see WebDriverScope
35+
*/
36+
class WebDriverContextCustomizerFactory implements ContextCustomizerFactory {
37+
38+
@Override
39+
public ContextCustomizer createContextCustomizer(Class<?> testClass,
40+
List<ContextConfigurationAttributes> configAttributes) {
41+
return new Customizer();
42+
}
43+
44+
private static class Customizer implements ContextCustomizer {
45+
46+
@Override
47+
public void customizeContext(ConfigurableApplicationContext context,
48+
MergedContextConfiguration mergedConfig) {
49+
WebDriverScope.registerWith(context);
50+
}
51+
52+
@Override
53+
public int hashCode() {
54+
return getClass().hashCode();
55+
}
56+
57+
@Override
58+
public boolean equals(Object obj) {
59+
if (obj == this) {
60+
return true;
61+
}
62+
if (obj == null || !obj.getClass().equals(getClass())) {
63+
return false;
64+
}
65+
return true;
66+
}
67+
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.servlet;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.openqa.selenium.WebDriver;
23+
24+
import org.springframework.beans.BeansException;
25+
import org.springframework.beans.factory.ObjectFactory;
26+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
27+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
28+
import org.springframework.beans.factory.config.Scope;
29+
import org.springframework.context.ApplicationContext;
30+
import org.springframework.context.ConfigurableApplicationContext;
31+
import org.springframework.util.ClassUtils;
32+
33+
/**
34+
* A special scope used for {@link WebDriver} beans. Usually registered by a
35+
* {@link WebDriverContextCustomizerFactory} and reset by a
36+
* {@link WebDriverTestExecutionListener}.
37+
*
38+
* @author Phillip Webb
39+
* @see WebDriverContextCustomizerFactory
40+
* @see WebDriverTestExecutionListener
41+
*/
42+
class WebDriverScope implements Scope {
43+
44+
public static final String NAME = "webDriver";
45+
46+
private static final String WEB_DRIVER_CLASS = "org.openqa.selenium.WebDriver";
47+
48+
private static final String[] BEAN_CLASSES = { WEB_DRIVER_CLASS,
49+
"org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder" };
50+
51+
private Map<String, Object> instances = new HashMap<String, Object>();
52+
53+
@Override
54+
public Object get(String name, ObjectFactory<?> objectFactory) {
55+
synchronized (this.instances) {
56+
Object instance = this.instances.get(name);
57+
if (instance == null) {
58+
instance = objectFactory.getObject();
59+
this.instances.put(name, instance);
60+
}
61+
return instance;
62+
}
63+
}
64+
65+
@Override
66+
public Object remove(String name) {
67+
synchronized (this.instances) {
68+
return this.instances.remove(name);
69+
}
70+
}
71+
72+
@Override
73+
public void registerDestructionCallback(String name, Runnable callback) {
74+
}
75+
76+
@Override
77+
public Object resolveContextualObject(String key) {
78+
return null;
79+
}
80+
81+
@Override
82+
public String getConversationId() {
83+
return null;
84+
}
85+
86+
/**
87+
* Reset all instances in the scope.
88+
* @return {@code true} if items were reset
89+
*/
90+
public boolean reset() {
91+
boolean reset = false;
92+
synchronized (this.instances) {
93+
for (Object instance : this.instances.values()) {
94+
reset = true;
95+
if (instance instanceof WebDriver) {
96+
((WebDriver) instance).quit();
97+
}
98+
}
99+
this.instances.clear();
100+
}
101+
return reset;
102+
}
103+
104+
/**
105+
* Register this scope with the specified context and reassign appropriate bean
106+
* definitions to used it.
107+
* @param context the application context
108+
*/
109+
public static void registerWith(ConfigurableApplicationContext context) {
110+
if (!ClassUtils.isPresent(WEB_DRIVER_CLASS, null)) {
111+
return;
112+
}
113+
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
114+
if (beanFactory.getRegisteredScope(NAME) == null) {
115+
beanFactory.registerScope(NAME, new WebDriverScope());
116+
}
117+
context.addBeanFactoryPostProcessor(new BeanFactoryPostProcessor() {
118+
119+
@Override
120+
public void postProcessBeanFactory(
121+
ConfigurableListableBeanFactory beanFactory) throws BeansException {
122+
for (String beanClass : BEAN_CLASSES) {
123+
for (String beanName : beanFactory.getBeanNamesForType(
124+
ClassUtils.resolveClassName(beanClass, null))) {
125+
beanFactory.getBeanDefinition(beanName).setScope(NAME);
126+
127+
}
128+
}
129+
}
130+
131+
});
132+
}
133+
134+
/**
135+
* Return the {@link WebDriverScope} being used by the specified context (if any).
136+
* @param context the application context
137+
* @return the web driver scope or {@code null}
138+
*/
139+
public static WebDriverScope getFrom(ApplicationContext context) {
140+
if (context instanceof ConfigurableApplicationContext) {
141+
Scope scope = ((ConfigurableApplicationContext) context).getBeanFactory()
142+
.getRegisteredScope(NAME);
143+
return (scope instanceof WebDriverScope ? (WebDriverScope) scope : null);
144+
}
145+
return null;
146+
}
147+
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.servlet;
18+
19+
import org.springframework.test.context.TestContext;
20+
import org.springframework.test.context.TestExecutionListener;
21+
import org.springframework.test.context.support.AbstractTestExecutionListener;
22+
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
23+
24+
/**
25+
* {@link TestExecutionListener} to reset the {@link WebDriverScope}.
26+
*
27+
* @author Phillip Webb
28+
* @see WebDriverContextCustomizerFactory
29+
* @see WebDriverScope
30+
*/
31+
class WebDriverTestExecutionListener extends AbstractTestExecutionListener {
32+
33+
@Override
34+
public void afterTestMethod(TestContext testContext) throws Exception {
35+
WebDriverScope scope = WebDriverScope
36+
.getFrom(testContext.getApplicationContext());
37+
if (scope != null && scope.reset()) {
38+
testContext.setAttribute(
39+
DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
40+
Boolean.TRUE);
41+
}
42+
}
43+
44+
}

spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExe
7575
org.springframework.test.context.ContextCustomizerFactory=\
7676
org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory,\
7777
org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizerFactory,\
78-
org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory
78+
org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory,\
79+
org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory
7980

8081
# Test Execution Listeners
8182
org.springframework.test.context.TestExecutionListener=\
8283
org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener,\
83-
org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener
84+
org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener,\
85+
org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener

spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestWebDriverIntegrationTests.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@
1616

1717
package org.springframework.boot.test.autoconfigure.web.servlet;
1818

19+
import org.junit.FixMethodOrder;
1920
import org.junit.Test;
2021
import org.junit.runner.RunWith;
22+
import org.junit.runners.MethodSorters;
2123
import org.openqa.selenium.By;
24+
import org.openqa.selenium.NoSuchWindowException;
2225
import org.openqa.selenium.WebDriver;
2326
import org.openqa.selenium.WebElement;
2427

2528
import org.springframework.beans.factory.annotation.Autowired;
2629
import org.springframework.test.context.junit4.SpringRunner;
30+
import org.springframework.test.util.ReflectionTestUtils;
2731

2832
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.junit.Assert.fail;
2934

3035
/**
3136
* Tests for {@link WebMvcTest} with {@link WebDriver}.
@@ -34,8 +39,11 @@
3439
*/
3540
@RunWith(SpringRunner.class)
3641
@WebMvcTest(secure = false)
42+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
3743
public class WebMvcTestWebDriverIntegrationTests {
3844

45+
private static WebDriver previousWebDriver;
46+
3947
@Autowired
4048
private WebDriver webDriver;
4149

@@ -44,6 +52,22 @@ public void shouldAutoConfigureWebClient() throws Exception {
4452
this.webDriver.get("/html");
4553
WebElement element = this.webDriver.findElement(By.tagName("body"));
4654
assertThat(element.getText()).isEqualTo("Hello");
55+
WebMvcTestWebDriverIntegrationTests.previousWebDriver = this.webDriver;
56+
}
57+
58+
@Test
59+
public void shouldBeADifferentWebClient() throws Exception {
60+
this.webDriver.get("/html");
61+
WebElement element = this.webDriver.findElement(By.tagName("body"));
62+
assertThat(element.getText()).isEqualTo("Hello");
63+
try {
64+
ReflectionTestUtils.invokeMethod(previousWebDriver, "getCurrentWindow");
65+
fail("Did not call quit()");
66+
}
67+
catch (NoSuchWindowException ex) {
68+
// Expected
69+
}
70+
assertThat(previousWebDriver).isNotNull().isNotSameAs(this.webDriver);
4771
}
4872

4973
}

0 commit comments

Comments
 (0)