Teste de Integração com Spring Boot, Kotlin e Spring Security
Os testes de integração verificam se todos os componentes da aplicação funcionam juntos: Controller, Security, Service, Repository e Banco de Dados.
Neste exemplo utilizaremos:
- Spring Boot
- Kotlin
- Spring Security
- MockMvc
- MySQL para testes
- JUnit 5
Dependência
Adicione a dependência do Spring Security Test em seu build.gradle.kts:
testImplementation("org.springframework.security:spring-security-test:7.1.0")Banco de dados de teste
Crie o arquivo src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mercadolivrotest?createDatabaseIfNotExist=true
username: root
password: root
jpa:
hibernate:
ddl-auto: update
jackson:
default-property-inclusion: non_null
jwt:
secret: 7c34debd-b72f-4941-96cb-89aaeafc029d
expiration: 3600000Quando utilizamos o profile test, o Spring carregará esse arquivo automaticamente.
Classe de teste
package mercado_livro.controller
import com.fasterxml.jackson.databind.ObjectMapper
import mercado_livro.controller.request.PostCustomerRequest
import mercado_livro.helper.buildCustomers
import mercado_livro.repository.CustomerRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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.http.MediaType
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*
import kotlin.random.Random
import kotlin.test.assertEquals
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@WithMockUser
class CustomerControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var customerRepository: CustomerRepository
@Autowired
private lateinit var objectMapper: ObjectMapper
@BeforeEach
fun seTup() = customerRepository.deleteAll()
@AfterEach
fun tearDown() = customerRepository.deleteAll()
@Test
fun `should create customer`() {
val request = PostCustomerRequest("fake name","${Random.nextInt()}@fake.com","1234")
mockMvc.perform(
post("/customer")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isCreated)
val customers = customerRepository.findAll().toList()
assertEquals(1,customers.size)
assertEquals(request.name,customers[0].name)
assertEquals(request.email,customers[0].email)
}
@Test
fun `should throw error when customer has invalid information`() {
val request = PostCustomerRequest("","${Random.nextInt()}@fake.com","1234")
mockMvc.perform(
post("/customer")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isUnprocessableEntity)
.andExpect(jsonPath("$.httpCode").value(422))
.andExpect(jsonPath("$.message").value("Invalid request"))
.andExpect(jsonPath("$.internalCode").value("ML001"))
val customers = customerRepository.findAll().toList()
assertEquals(0,customers.size)
assertEquals(request.name,"")
}
@Test
@WithMockUser(roles = ["ADMIN"])
fun `should throw an error when user is not found by id`() {
val id = UUID.randomUUID().toString()
val user1 = buildCustomers(
name = "Gustavo",
)
customerRepository.save(user1)
mockMvc.perform(
get("/customer/$id")
)
.andExpect(status().isNotFound)
.andExpect(jsonPath("$.httpCode").value(404))
.andExpect(jsonPath("$.message").value("Customer {$id} not exists"))
.andExpect(jsonPath("$.internalCode").value("ML_201"))
}
}Entendendo as anotações
@SpringBootTest
Ela sobe praticamente toda a aplicação:
- Controllers
- Services
- Repositories
- Security
- Bean Validation
- JPA
- Banco de dados
- Configurações do Spring
Ou seja, o teste roda em um ambiente muito parecido com a aplicação real.
Sem essa anotação seria necessário mockar praticamente tudo.
@AutoConfigureMockMvc
Cria automaticamente um objeto MockMvc. Com ele conseguimos realizar chamadas HTTP.
@ActiveProfiles(“test”)
Informa ao Spring qual profile será utilizado. Nesse caso: application-test.yml
@WithMockUser
A anotação @WithMockUser serve para simular um usuário autenticado no Spring Security durante a execução de testes. Em uma CustomerControllerTest, ela permite testar endpoints protegidos sem precisar de login real, formulário ou token.
@WithMockUser(roles = [“ADMIN”])
cria um usuário autenticado com:
ROLE_ADMIN
Isso é extremamente útil para testar endpoints protegidos pelo Spring Security sem precisar gerar JWTs reais.
with(user(UserCustomDetails(user)))
Outra forma de se autenticar nos testes. o UserCustomDetails, O UserCustomDetails é uma implementação da interface UserDetails, utilizada pelo Spring Security para representar um usuário autenticado. Sempre que um usuário faz login, o Spring trabalha com um objeto que implementa essa interface para obter informações como nome de usuário, senha, permissões e status da conta. Um exemplo dessa classe:
package mercado_livro.security
import mercado_livro.enums.CustomerStatus
import mercado_livro.model.CustomerModel
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class UserCustomDetails (
private val customerModel: CustomerModel
):UserDetails{
val id:String= customerModel.id!!
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = customerModel.roles!!.map { SimpleGrantedAuthority(it.description) }.toMutableList()
override fun getPassword(): String =customerModel.password
override fun getUsername(): String = customerModel.id!!
override fun isEnabled(): Boolean = customerModel.status==CustomerStatus.ACTIVE
}Um teste que chama esse with(user(UserCustomDetails(user))):
@Test
fun `should return forbidden when user has different id`() {
val id = UUID.randomUUID().toString()
val user1 = buildCustomers(
name = "Gustavo",
)
customerRepository.save(user1)
mockMvc.perform(
get("/customer/$id").with(user(UserCustomDetails(user1)))
)
.andExpect(status().isForbidden)
.andExpect(jsonPath("$.httpCode").value(403))
.andExpect(jsonPath("$.message").value("Unauthorized"))
.andExpect(jsonPath("$.internalCode").value("ML000"))
}andExpect()
utilizamos os métodos andExpect() para verificar se a API respondeu exatamente como esperado.
assertEquals
O método assertEquals() é utilizado para comparar o valor esperado com o valor obtido durante a execução do teste. Se os dois valores forem iguais, o teste continua normalmente; caso contrário, o teste falha.
A classe PostCustomerRequest:
package mercado_livro.controller.request
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotEmpty
import mercado_livro.validation.EmailAvailable
data class PostCustomerRequest(
@field:NotEmpty(message = "Nome não por ser vazio")
var name:String,
@field:Email(message = "Email inválido")
@EmailAvailable
var email:String,
@field:NotEmpty(message = "Senha deve ser informada")
var password:String
)A funçã0 buildCustomers:
package mercado_livro.helper
import mercado_livro.enums.CustomerStatus
import mercado_livro.enums.Roles
import mercado_livro.model.BookModel
import mercado_livro.model.CustomerModel
import mercado_livro.model.PurchaseModel
import java.math.BigDecimal
import java.util.*
fun buildCustomers(
id:String? = null,
name:String = "Customer name",
email:String="${UUID.randomUUID()}@email.com",
password:String="password",
roles:Set<Roles>?=null
): CustomerModel {
return CustomerModel(
id=id,
name=name,
email=email,
status = CustomerStatus.ACTIVE,
password=password,
roles = roles ?: setOf(Roles.CUSTOMER)
)
}
0 comentários