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!
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!
Quando utilizamos o Spring Boot 4 com @AutoConfigureMockMvc, a integração é ainda mais simples:
- Se o Spring Security estiver presente no classpath, o
@AutoConfigureMockMvc aplica o springSecurity() automaticamente ao construir o bean MockMvc!
- 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.