Reputation: 400
I'm currently in the process of writing tests for my controllers in a Spring Boot project that uses WebSockets.
As information on the subject is hard to come by, my only lead is this example recommended by the docs.
Allow me to attempt to explain my forays so far, trying to understand and get my test environment set up. I'm following the context-based approach, and I'm torn between @WebMvcTest
and @ContextConfiguration
(which the example uses).
My motivation behind using @WebMvcTest
at all was this line from the Spring Boot docs:
When testing Spring Boot applications, this [using
@ContextConfiguration(classes=…)
in order to specify which Spring@Configuration
to load, or using nested@Configuration
classes within your test] is often not required. Spring Boot’s@*Test
annotations search for your primary configuration automatically whenever you do not explicitly define one.
@WebMvcTest
thus seems particularly suited to the task as it focuses only on the web layer by limiting the set of scanned beans to only those necessary (e.g. @controller
) rather than spinning up a complete ApplicationContext
.
The code example I'm following uses field injection to initialize channel interceptors to capture messages sent through them.
@Autowired private AbstractSubscribableChannel clientInboundChannel;
@Autowired private AbstractSubscribableChannel clientOutboundChannel;
@Autowired private AbstractSubscribableChannel brokerChannel;
As far as I can tell, these fields are what makes the presence of the TestConfig
class (see code block at the end for full class definition) in the example necessary, since without it, I get an error saying no beans qualify as autowire candidates. I believe these two fields in TestConfig
are the key:
@Autowired
private List<SubscribableChannel> channels;
@Autowired
private List<MessageHandler> handlers;
However, without @ContextConfiguration(classes = [WebSocketConfig::class])
(WebSocketConfig
being my own WebSocket configuration file), these two fields are always null
resulting in errors.
So far, this would mean that @ContextConfiguration(classes = [WebSocketConfig::class])
in combination with the presence of TestConfig
are needed.
The interesting thing is that without @WebMvcTest
, clientInboundChannel
, clientOutboundChannel
, and brokerChannel
are never actually initialized. So what this leaves me with is that I need both @WebMvcTest
and @ContextConfiguration
, which somehow seems odd.
And with the last update to the example repo being more than two years old, I can't shake the feeling that it may be somewhat outdated.
This is what my test class (Kotlin) currently looks like. I've omitted the createRoom
test case for brevity:
@WebMvcTest(controllers = [RoomController::class])
@ContextConfiguration(classes = [WebSocketConfig::class, RoomControllerTests.TestConfig::class])
class RoomControllerTests {
@Autowired
private lateinit var clientInboundChannel: AbstractSubscribableChannel
@Autowired
private lateinit var clientOutboundChannel: AbstractSubscribableChannel
@Autowired
private lateinit var brokerChannel: AbstractSubscribableChannel
private lateinit var clientOutboundChannelInterceptor: TestChannelInterceptor
private lateinit var brokerChannelInterceptor: TestChannelInterceptor
private lateinit var sessionId: String
@BeforeEach
fun setUp() {
brokerChannelInterceptor = TestChannelInterceptor()
clientOutboundChannelInterceptor = TestChannelInterceptor()
brokerChannel.addInterceptor(brokerChannelInterceptor)
clientOutboundChannel.addInterceptor(clientOutboundChannelInterceptor)
}
@Test
fun createRoom() {
// test room creation
// ...
}
@Configuration
internal class TestConfig : ApplicationListener<ContextRefreshedEvent?> {
@Autowired
private val channels: List<SubscribableChannel>? = null
@Autowired
private val handlers: List<MessageHandler>? = null
override fun onApplicationEvent(event: ContextRefreshedEvent) {
for (handler in handlers!!) {
if (handler is SimpAnnotationMethodMessageHandler) {
continue
}
for (channel in channels!!) {
channel.unsubscribe(handler)
}
}
}
}
}
Upvotes: 3
Views: 4639
Reputation: 7865
@ContextConfiguration
is to load and configure an ApplicationContext for integration tests as per here.
@ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests.
For @WebMvcTest
, to create any collaborators use @MockBean
or @Import
as per here.
Typically @WebMvcTest is used in combination with @MockBean or @Import to create any collaborators required by your @Controller beans.
Upvotes: 1
Reputation: 400
The answer posted here was my first clue towards starting to understand how @WebMvcTest
and @ContextConfiguration
are meant to be used.
It would appear the line that is really necessary is @ContextConfiguration(classes = [WebSocketConfig::class])
, since without it, the necessary configuration (my own WebSocketConfig
, in this case) can't be found.
While @WebMvcTest
(and test slices in general) does automatically search for the primary configuration, the configuration defined by the main application class is the one that's found since that class is the one annotated with @SpringBootApplication
.
What's more, test slices such as @WebMvcTest
flat out exclude @configuration
classes from scanning, meaning they won't be included in the application context loaded by the test slice.
Supporting this is the fact that the test works if @WebMvcTest
is replaced with @SpringBootTest
— even without @ContextConfiguration(classes = [WebSocketConfig::class])
— meaning that WebSocketConfig
was loaded.
To summarize, as I see it, there are 3 ways to include a non-primary configuration in the application context so that it's detected in integration tests:
@SpringBootTest
(and optionally @ContextConfiguration
to restrict the number of loaded beans to only those needed)@WebMvcTest
) in conjunction with @ContextConfiguration
@WebMvcTest
) and define the configuration in the main class (i.e. the one annotated with @SpringBootApplication
)Upvotes: 5