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.

Testes Unitários com Spring Boot, Kotlin, MockK e JaCoCo

Introdução

Os testes unitários são fundamentais para garantir a qualidade do código, facilitar refatorações e reduzir a quantidade de bugs durante o desenvolvimento.

Neste tutorial será apresentado como criar testes unitários em uma aplicação Spring Boot + Kotlin, utilizando:

  • JUnit 5
  • MockK
  • JaCoCo
  • Gradle Kotlin DSL

Ao final você será capaz de testar serviços, criar mocks, verificar chamadas aos repositórios e gerar um relatório de cobertura de código.

Dependências

No arquivo build.gradle.kts adicione as seguintes dependências:

KOTLIN
plugins {
	kotlin("jvm") version "1.9.25"
	kotlin("plugin.spring") version "1.9.25"
	id("org.springframework.boot") version "3.5.14"
	id("io.spring.dependency-management") version "1.1.7"
	kotlin("plugin.jpa")version "1.4.21"
	jacoco

}

dependencies {

	testImplementation("io.mockk:mockk:1.14.11")

}

Adicionar application-test.yml

Dentro da pasta src->test crie uma pasta resources como o arquivo application-test.yml.

Este arquivo vai servir para testes unitários e integrados, neste capitulo vamos ver o unitário no próximo vamos ver o integrado.

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

Estrutura do teste

Primeiro nossa função que constrói os customers para nós:

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"
): CustomerModel {
    return CustomerModel(
        id=id,
        name=name,
        email=email,
        status = CustomerStatus.ACTIVE,
        password=password,
        roles = setOf(Roles.CUSTOMER)
    )
}

A classe de teste utilizada neste exemplo é:

KOTLIN
@ExtendWith(MockKExtension::class)
class CustomerServiceTest {

    @InjectMockKs
    @SpyK
    private lateinit var customerService: CustomerService

    @MockK
    private lateinit var customerRepository: CustomerRepository
    @MockK
    private lateinit var bookService: BookService
    @MockK
    private lateinit var bcrypt: BCryptPasswordEncoder

    @Test
    fun `should return all customers`() {
        val fakeCustomers= listOf(buildCustomers(),buildCustomers())
        every { customerRepository.findAll() } returns fakeCustomers

        val customers= customerService.getAll(null)

        assertEquals(fakeCustomers,customers)
        verify(exactly = 1) { customerRepository.findAll()  }
        verify(exactly = 0) { customerRepository.findByNameContaining(any())  }

    }

    @Test
    fun `should return customers when name is informed`() {
        val name= UUID.randomUUID().toString()

        val fakeCustomers= listOf(buildCustomers(),buildCustomers())
        every { customerRepository.findByNameContaining(name) } returns fakeCustomers

        val customers= customerService.getAll(name)

        assertEquals(fakeCustomers,customers)
        verify(exactly = 0) { customerRepository.findAll()  }
        verify(exactly = 1) { customerRepository.findByNameContaining(any())  }

    }

    @Test
    fun `should created customer and encrypt password`(){
        val initialPassword = Math.random().toString()
        val fakeCustomer = buildCustomers(password = initialPassword)
        val fakePassword = UUID.randomUUID().toString()
        val fakeCustomerEncrypt=fakeCustomer.copy(password = fakePassword)

        every { customerRepository.save(fakeCustomerEncrypt) }returns fakeCustomer
        every { bcrypt.encode(initialPassword) }returns fakePassword

        customerService.createCustomer(fakeCustomer)

        verify(exactly = 1)  { customerRepository.save(fakeCustomerEncrypt) }
        verify(exactly = 1) {bcrypt.encode(initialPassword)  }
    }

    @Test
    fun `should return customer by id`(){
        val id = UUID.randomUUID()
        val fakeCustomer = buildCustomers(id=id.toString())

        every { customerRepository.findById(id.toString()) }returns Optional.of(fakeCustomer)

        val customer = customerService.getById(id)
        assertEquals(fakeCustomer,customer)

        verify(exactly = 1) { customerRepository.findById(id.toString()) }
    }

    @Test
    fun `should throw error when customer not found`(){
        val id = UUID.randomUUID()
        every { customerRepository.findById(id.toString()) }returns Optional.empty()

        val error = assertThrows<NotFoundException> {
            customerService.getById(id)
        }

        assertEquals("Customer {${id}} not exists",error.message)
        assertEquals("ML_201",error.errorCode)
        verify(exactly = 1) { customerRepository.findById(id.toString()) }
    }

    @Test
    fun `should update customer`(){
        val id = UUID.randomUUID()
        val fakeCustomer = buildCustomers(id=id.toString())

        every { customerRepository.existsById(id.toString()) }returns true
        every { customerRepository.save(fakeCustomer) } returns fakeCustomer

        customerService.updateCustomer(fakeCustomer)

        verify(exactly = 1) { customerRepository.save(fakeCustomer) }
        verify(exactly = 1) { customerRepository.existsById(id.toString()) }
    }

    @Test
    fun `should throw not found when update customer`(){
        val id = UUID.randomUUID()
        val fakeCustomer = buildCustomers(id=id.toString())

        every { customerRepository.existsById(id.toString()) }returns false
        every { customerRepository.save(fakeCustomer) } returns fakeCustomer

        val error = assertThrows<NotFoundException> {
            customerService.updateCustomer(fakeCustomer)
        }

        assertEquals("Customer {${id}} not exists",error.message)
        assertEquals("ML_201",error.errorCode)
        verify(exactly = 0) { customerRepository.save(fakeCustomer) }
        verify(exactly = 1) { customerRepository.existsById(id.toString()) }
    }

    @Test
    fun `should delete customer`(){
        val id = UUID.randomUUID()
        val fakeCustomer = buildCustomers(id=id.toString())
        val expectedCustomer = fakeCustomer.copy(
            status = CustomerStatus.INACTIVE
        )

        every { customerService.getById(id) } returns fakeCustomer
        every{customerRepository.save(expectedCustomer)} returns expectedCustomer
        every { bookService.deleteByCustomer(fakeCustomer) } just runs
        every { customerRepository.existsById(id.toString()) } returns true

        customerService.deleteCustomer(id)

        verify(exactly = 1) { customerRepository.save(expectedCustomer) }
        verify(exactly = 1) { bookService.deleteByCustomer(fakeCustomer) }
    }

    @Test
    fun `should throw not found exception when delete customer`(){
        val id = UUID.randomUUID()

        every { customerService.getById(id) } throws NotFoundException(
            Errors.ML201.message.format(id),
            Errors.ML201.code)

        val error = assertThrows<NotFoundException> {
            customerService.deleteCustomer(id)
        }

        assertEquals("Customer {${id}} not exists",error.message)
        assertEquals("ML_201",error.errorCode)
        verify(exactly = 1) { customerService.getById(id) }
        verify(exactly = 0) { customerRepository.save(any()) }
        verify(exactly = 0) { bookService.deleteByCustomer(any()) }
    }

    @Test
    fun `should return true when email available`(){
        val email = "${UUID.randomUUID()}@email.com"

        every { customerRepository.existsByEmail(email) } returns false

        val emailAvailable = customerService.emailAvailable(email)
        assertTrue(emailAvailable)

        verify(exactly = 1) { customerRepository.existsByEmail(email) }
    }

    @Test
    fun `should return false when email available`(){
        val email = "${UUID.randomUUID()}@email.com"

        every { customerRepository.existsByEmail(email) } returns true

        val emailAvailable = customerService.emailAvailable(email)
        assertFalse(emailAvailable)

        verify(exactly = 1) { customerRepository.existsByEmail(email) }
    }

}

Explicando as anotações

@ExtendWith(MockKExtension::class)

Inicializa automaticamente todos os mocks do MockK.

Sem ela seria necessário chamar:

KOTLIN
MockKAnnotations.init(this)


@MockK

Cria um objeto falso (Mock).

KOTLIN
@MockK
private lateinit var customerRepository: CustomerRepository

Todo acesso ao repositório será simulado. Nenhuma consulta ao banco será realizada.

@InjectMockKs

Cria automaticamente a classe que será testada e injeta todos os mocks nela.

KOTLIN
@InjectMockKs
private lateinit var customerService: CustomerService

É equivalente a:

KOTLIN
val customerService = CustomerService(
                        customerRepository,
                        bookService,
                        bcrypt )

@SpyK

Cria um Spy.

Um Spy executa o código real, porém permite sobrescrever alguns métodos. Foi utilizado porque o método deleteCustomer() chama outro método da própria classe (getById()).

KOTLIN
every { customerService.getById(id) } returns fakeCustomer

every

Serve para informar o comportamento esperado de um mock. Exemplo:

KOTLIN
every { customerRepository.findAll() } returns customers

Sempre que o método findAll() for chamado será retornada a lista customers.

verify

Verifica quantas vezes um método foi executado.

KOTLIN
verify(exactly = 1) { customerRepository.findAll() }

Também é possível verificar que um método nunca foi chamado.

KOTLIN
verify(exactly = 0) { customerRepository.findByNameContaining(any())  }

assertEquals

O assertEquals é uma função utilizada para verificar se dois valores são iguais. Se os valores forem diferentes, o teste falha automaticamente.Exemplo:

KOTLIN
assertEquals(fakeCustomers,customers)

Testando exceções

Para saber como tratamos as exceções clique aqui. Quando um cliente não existir:

KOTLIN
every { customerService.getById(id) } throws NotFoundException(
            Errors.ML201.message.format(id),
            Errors.ML201.code)

        val error = assertThrows<NotFoundException> {
            customerService.deleteCustomer(id)
        }

        assertEquals("Customer {${id}} not exists",error.message)
        assertEquals("ML_201",error.errorCode)

Testando exclusão lógica

Como deleteByCustomer() não retorna nada, utilizamos:

KOTLIN
every { bookService.deleteByCustomer(customer) } just runs

Esse comando informa ao MockK que o método apenas executa sem retornar valor.

Testando métodos booleanos

Exemplo:

KOTLIN
every { customerRepository.existsByEmail(email) } returns false

Depois:

KOTLIN
assertTrue(customerService.emailAvailable(email))

No cenário contrário:

KOTLIN
every { customerRepository.existsByEmail(email) } returns true
assertFalse(customerService.emailAvailable(email))

Para ver a cobertura de código basta clicar nos itens à direita do gradle verification, depois ir até a página à esquerda index.html abrir e colocar para abrir em algum browser.

Veja também mockKStatic e slot, clique aqui. Para ver testes de integração, clique aqui.

0 comentários

Deixe seu comentário