Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@WebAppConfiguration fails with ServletServerContainerFactoryBean in the ApplicationContext [SPR-14367] #18939

Closed
spring-projects-issues opened this issue Jun 15, 2016 · 9 comments

Comments

@spring-projects-issues
Copy link
Collaborator

@spring-projects-issues spring-projects-issues commented Jun 15, 2016

Craig opened SPR-14367 and commented

The servlet container used by unit tests doesn't support websockets.

To see this issue, define this configuration class (as documented at http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-runtime-configuration )

@Configuration
@EnableWebSocket
public class WebSocketConfig {

	private static final int MAX_MESSAGE_SIZE = 16 * 1024 * 1024;

	@Bean
	public ServletServerContainerFactoryBean createWebSocketContainer() {
		ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
		container.setMaxTextMessageBufferSize(MAX_MESSAGE_SIZE);
		container.setMaxBinaryMessageBufferSize(MAX_MESSAGE_SIZE);
		return container;
	}
}

then run this unit test (which is created by the Spring boot web application archetype):

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ExampleApplication.class)
@WebAppConfiguration
public class ExampleApplicationTests {

	@Test
	public void contextLoads() {
	}

}

The result is that the test fails with this stack trace:

Caused by: java.lang.IllegalStateException: A ServletContext is required to access the javax.websocket.server.ServerContainer instance
	at org.springframework.util.Assert.state(Assert.java:392)
	at org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean.afterPropertiesSet(ServletServerContainerFactoryBean.java:99)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:753)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:766)
	at org.springframework.boot.SpringApplication.createAndRefreshContext(SpringApplication.java:361)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
	at org.springframework.boot.test.SpringApplicationContextLoader.loadContext(SpringApplicationContextLoader.java:98)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:183)
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:123)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:228)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:230)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:249)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
	at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:283)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeWithRerun(JUnit4Provider.java:173)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:153)
	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:128)
	at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:203)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:155)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:103)

The problem is that the org.springframework.mock.web.MockServletContext instance (referenced in the stack trace frame "org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean.afterPropertiesSet(ServletServerContainerFactoryBean.java:99)") doesn't have the servlet attribute "javax.websocket.server.ServerContainer" set to an instance of javax.websocket.server.ServerContainer (the attribute is instead set to null).

As a workaround, the test class can be annotated with:

@org.springframework.boot.test.IntegrationTest("server.port:0")

causing a real servlet context (which does support websockets) to be used instead of a mock one.


Affects: 4.2.6

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Jun 16, 2016

Rossen Stoyanchev commented

The Spring TestContext framework support and the @WebAppConfiguration annotation is container-less testing by definition. Essentially the mock request and response, not an actual Servlet container. Were you expecting an actual ServerContainer or some mock version that would simply prevent your configuration from failing to load?

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Jun 16, 2016

Craig commented

I wouldn't expect it to provide a real ServerContainer because, as you said, that would require a full servlet container to be running. I would expect it to return a mock that provides the necessary features to perform unit testing. There is precedent for such mocks; for example, MockServletContext already includes use of MockSessionCookieConfig and MockRequestDispatcher.

It was pretty surprising to me that using code given in the Spring docs causes a unit test provided by default with new Spring projects to fail :)

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Jun 16, 2016

Rossen Stoyanchev commented

Certainly we need to make sure the above configuration at least doesn't fail. I'm still not sure what else makes sense beyond that.

For example we could insert a package private, empty stub (no-op) implementation of ServerContainer as the request attribute. That will allow the above configuration to load but calls to the ServerContainer would be ignored. I see no way meaningful way to implement the connectToServer methods in a mock scenario as we can't actually connect. As for the addEndpoint methods there is not much to do there either. For once Spring's own WebSocket support does not rely on such registrations and uses a RequestUpgradeStrategy for each server. For straight-up JSR-356 Endpoint implementations, such as the ones that would be registered by the factory bean, there is nothing we can do outside a container.

I see this strictly as a request to ensure configuration such as the above does not fail and prevent tests from running.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Jun 16, 2016

Rossen Stoyanchev commented

Updated title (was: "Unit testing servlet container should support websockets").

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Jun 22, 2016

Sam Brannen commented

Resolved as described in GitHub commit f7dd757:

Support WebSocket ServletServerContainerFB in the TCF

Prior to this commit, any attempt to include a bean of type
ServletServerContainerFactoryBean in the WebApplicationContext for an
integration test class annotated with @WebAppConfiguration in
conjunction the Spring TestContext Framework (TCF) would have resulted
in an IllegalStateException stating that "A ServletContext is required
to access the javax.websocket.server.ServerContainer instance."

In such scenarios, the MockServletContext was in fact present in the
WebApplicationContext; however there was no WebSocket ServerContainer
stored in the ServletContext.

This commit addresses this issue by introducing the following.

  • MockServerContainer: a private mock implementation of the
    javax.websocket.server.ServerContainer interface.

  • MockServerContainerContextCustomizer: a ContextCustomizer that
    instantiates a new MockServerContainer and stores it in the
    ServletContext under the attribute named
    "javax.websocket.server.ServerContainer".

  • MockServerContainerContextCustomizerFactory: a
    ContextCustomizerFactory which creates a
    MockServerContainerContextCustomizer if WebSocket support is present
    in the classpath and the test class is annotated with
    @WebAppConfiguration. This factory is registered by default via the
    spring.factories mechanism.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Nov 9, 2016

xiaokun commented

This error still occurs in version 4.3.3 in spring mvc project.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Nov 9, 2016

xiaokun commented

Here are test code:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath*:applicationContext*.xml" })
public class Test{

}
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'createWebSocketContainer' defined in net.ebaolife.tpa.websocket.WebSocketConfig: Invocation of init method failed; nested exception is java.lang.IllegalStateException: A ServletContext is required to access the javax.websocket.server.ServerContainer instance
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1583)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:732)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:861)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:128)
	at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:108)
	at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:251)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
	... 24 more
Caused by: java.lang.IllegalStateException: A ServletContext is required to access the javax.websocket.server.ServerContainer instance
	at org.springframework.util.Assert.state(Assert.java:392)
	at org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean.afterPropertiesSet(ServletServerContainerFactoryBean.java:102)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1642)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1579)
	... 39 more
@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Nov 9, 2016

xiaokun commented

In test class, add @WebAppConfiguration, this error is gone.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Nov 14, 2016

Rossen Stoyanchev commented

Once the fix version is released, the ticket is permanently closed. If you're still having an issue you need to open a new ticket with the relevant version and a fresh description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants