Omar Sharaki
Omar Sharaki

Reputation: 400

@WebMvcTest vs @ContextConfiguration when testing WebSocket controllers

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

Answers (2)

jumping_monkey
jumping_monkey

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

Omar Sharaki
Omar Sharaki

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:

  • Annotate the test class with @SpringBootTest (and optionally @ContextConfiguration to restrict the number of loaded beans to only those needed)
  • Annotate the test class with a test slice (e.g. @WebMvcTest) in conjunction with @ContextConfiguration
  • Annotate the test class with a test slice (e.g. @WebMvcTest) and define the configuration in the main class (i.e. the one annotated with @SpringBootApplication)

Upvotes: 5

Related Questions