Rafael

Engenheiro de Software

Olá, Eu sou Rafael. Estou construindo coisas para a web e mobile.

Um desenvolvedor dedicado a criar experiências digitais de alto desempenho, acessíveis e limpas. Eu me especializo em transformar problemas complexos em soluções de engenharia elegantes.

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:

KOTLIN
testImplementation("org.springframework.security:spring-security-test:7.1.0")

Banco de dados de teste

Crie o arquivo src/test/resources/application-test.yml

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: 3600000

Quando utilizamos o profile test, o Spring carregará esse arquivo automaticamente.

Classe de teste

KOTLIN
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:

KOTLIN
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))):

KOTLIN
@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:

KOTLIN
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:

KOTLIN
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

Deixe seu comentário