Integrando o Spring Security ao MockMvc

Ao concluirmos esta aula, nós:

  • Saberemos como obter a dependência spring-security-test no Maven e no Gradle 9.
  • Compreenderemos como integrar o Spring Security ao Spring MVC Test framework (MockMvc) tanto em aplicações Spring Framework tradicionais quanto em aplicações Spring Boot 4.

Obtendo as Dependências Necessárias

Antes de podermos integrar o Spring Security com o MockMvc e utilizar anotações de simulação de usuário autenticado (como @WithMockUser e @WithUserDetails) ou post-processors de requisição (como csrf()), precisamos declarar a dependência spring-security-test no nosso script de build.

Se estivermos usando o Maven (pom.xml), adicionamos:

1
2
3
4
5
6
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>6.4.2</version>
    <scope>test</scope>
</dependency>

Se estivermos usando o Gradle 9 (Kotlin DSL — build.gradle.kts), adicionamos na nossa suíte de testes de integração:

1
2
3
dependencies {
    integrationTestImplementation("org.springframework.security:spring-security-test")
}

Importante: A versão da dependência spring-security-test deve ser sempre exatamente a mesma versão do Spring Security utilizada pela aplicação (quando usamos o plugin de gerenciamento de dependências do Spring Boot, basta omitir a versão para que o BOM do Spring Boot 4.1 resolva a versão compatível automaticamente!).

Vamos descobrir agora como configurar nossos testes de integração com o Spring Security!


Configurando Nossos Testes de Integração com Spring Security

1. Usando o Spring Framework Puro

Se estivermos usando o Spring Framework tradicional (configurando o MockMvc manualmente via MockMvcBuilders.webAppContextSetup), integramos o Spring Security seguindo dois passos:

Primeiro, registramos o listener WithSecurityContextTestExecutionListener na anotação @TestExecutionListeners. Esse listener é responsável por popular o SecurityContext antes da execução de cada método de teste (quando usamos anotações como @WithMockUser ou @WithUserDetails) e limpar o SecurityContextHolder após o término do método de teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [IntegrationTestContext::class])
@WebAppConfiguration
@TestExecutionListeners(
    value = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class,
        WithSecurityContextTestExecutionListener::class
    ]
)
@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindTaskWhenTaskIsFoundTest {

    @Autowired
    private lateinit var webAppContext: WebApplicationContext

    private lateinit var mockMvc: MockMvc
}

Segundo, precisamos garantir que o MockMvc passe todas as requisições pela cadeia de filtros do Spring Security (springSecurityFilterChain). Fazemos isso aplicando o configurador estático SecurityMockMvcConfigurers.springSecurity() dentro do método @BeforeEach ao construir o MockMvc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.testwithspring.intermediate.web

import com.github.springtestdbunit.DbUnitTestExecutionListener
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.testwithspring.intermediate.ReplacementDataSetLoader
import com.testwithspring.intermediate.config.Profiles
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
import org.springframework.test.context.transaction.TransactionalTestExecutionListener
import org.springframework.test.context.web.ServletTestExecutionListener
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [IntegrationTestContext::class])
@WebAppConfiguration
@TestExecutionListeners(
    value = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class,
        WithSecurityContextTestExecutionListener::class
    ]
)
@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindTaskWhenTaskIsFoundTest {

    @Autowired
    private lateinit var webAppContext: WebApplicationContext

    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
            .apply<DefaultMockMvcBuilder>(springSecurity())
            .build()
    }
}

O método .apply(springSecurity()) localiza o bean springSecurityFilterChain no WebApplicationContext, registra-o como filtro no MockMvc e garante que o SecurityContext criado pelo WithSecurityContextTestExecutionListener seja propagado em cada requisição HTTP!

2. Usando o Spring Boot 4 (@AutoConfigureMockMvc)

Quando utilizamos o Spring Boot 4 com @AutoConfigureMockMvc, a integração é ainda mais simples:

  1. Se o Spring Security estiver presente no classpath, o @AutoConfigureMockMvc aplica o springSecurity() automaticamente ao construir o bean MockMvc!
  2. Só precisamos garantir que o WithSecurityContextTestExecutionListener esteja registrado na nossa classe de teste (o que já acontece automaticamente se usarmos mergeMode = MergeMode.MERGE_WITH_DEFAULTS, ou declarando-o na lista de @TestExecutionListeners).

Veja como fica a nossa classe de teste de integração no Spring Boot 4:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.testwithspring.intermediate.web

import com.github.springtestdbunit.DbUnitTestExecutionListener
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.testwithspring.intermediate.ReplacementDataSetLoader
import com.testwithspring.intermediate.config.Profiles
import org.junit.jupiter.api.Tag
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
import org.springframework.test.context.transaction.TransactionalTestExecutionListener
import org.springframework.test.context.web.ServletTestExecutionListener
import org.springframework.test.web.servlet.MockMvc

@SpringBootTest(classes = [IntegrationTestContext::class])
@AutoConfigureMockMvc
@TestExecutionListeners(
    value = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class,
        WithSecurityContextTestExecutionListener::class
    ]
)
@DatabaseSetup("/com/testwithspring/intermediate/tasks.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@Tag("integrationTest")
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class FindTaskWhenTaskIsFoundTest(
    @Autowired private val mockMvc: MockMvc
) {
}

Dica: Caso em algum teste específico você queira desabilitar os filtros do Spring Security no MockMvc auto-configurado pelo Spring Boot, basta passar o atributo addFilters = false na anotação: @AutoConfigureMockMvc(addFilters = false).


Resumo

Esta aula nos ensinou quatro conceitos essenciais:

  • Declaramos a dependência spring-security-test no script de build para habilitar o suporte de testes do Spring Security.
  • O listener WithSecurityContextTestExecutionListener configura o SecurityContext antes da execução de cada método de teste e limpa o contexto de segurança após o término do método.
  • No Spring Framework puro, ativamos o filtro do Spring Security no MockMvc chamando .apply(springSecurity()) no MockMvcBuilders.webAppContextSetup(webAppContext).
  • Por padrão, o @AutoConfigureMockMvc do Spring Boot configura automaticamente os filtros do Spring Security no MockMvc sempre que o Spring Security está presente no classpath.