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:
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.
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: 3600000Estrutura do teste
Primeiro nossa função que constrói os customers para nós:
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 é:
@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:
MockKAnnotations.init(this)
@MockK
Cria um objeto falso (Mock).
@MockK
private lateinit var customerRepository: CustomerRepositoryTodo 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.
@InjectMockKs
private lateinit var customerService: CustomerServiceÉ equivalente a:
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()).
every { customerService.getById(id) } returns fakeCustomerevery
Serve para informar o comportamento esperado de um mock. Exemplo:
every { customerRepository.findAll() } returns customersSempre que o método findAll() for chamado será retornada a lista customers.
verify
Verifica quantas vezes um método foi executado.
verify(exactly = 1) { customerRepository.findAll() }Também é possível verificar que um método nunca foi chamado.
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:
assertEquals(fakeCustomers,customers)Testando exceções
Para saber como tratamos as exceções clique aqui. Quando um cliente não existir:
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:
every { bookService.deleteByCustomer(customer) } just runsEsse comando informa ao MockK que o método apenas executa sem retornar valor.
Testando métodos booleanos
Exemplo:
every { customerRepository.existsByEmail(email) } returns falseDepois:
assertTrue(customerService.emailAvailable(email))No cenário contrário:
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