🧪 P20: Simulador de Batalha (Testes Unitários Automatizados)
Bem-vindo à segunda missão da Fase 10: Robustez Técnica & Tempo Real! 🧪
Neste projeto vital focado em Garantia de Qualidade (QA) e boas práticas de engenharia de software, você aprenderá a criar rotinas de verificação automatizadas para testar a sua lógica de negócios e as equações matemáticas de ataque/defesa do app de forma rápida e sistemática, utilizando a biblioteca padrão de testes unitários JUnit 5 combinada à simulação de dublês de testes com a biblioteca MockK.
Tudo isso ilustrado na prática com uma arena de simulação de combate tático de heróis de quadrinhos (Marvel vs DC).
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P19: Banco de Wakanda
- 📘 Cap 21: Testes de Combate: Qualidade
🎯 Objetivo do Projeto
Aprender a garantir a integridade do código sob testes automatizados contendo:
- Isolamento de Lógica: Separar cálculos puramente matemáticos da interface Compose (arquitetura limpa).
- Casos de Teste JUnit: Escrever blocos de teste com anotações
@Testvalidando cenários comuns, limites de erro e bônus de dano. - Asserções Técnicas: Aplicar verificações do JUnit para provar que a equação retorna o valor exato previsto (
assertEquals). - Princípios TDD: Introduzir os fundamentos do ciclo de desenvolvimento orientado a testes (Test-Driven Development).
graph TD
A[Guerreiro ataca Ladino] --> B[BattleEngine.calcularDano ATQ=20, DEF=10]
B --> C[Executa Equação: Dano = ATQ - DEF/2 = 15]
C --> D[Interface renderiza o Dano na tela]
subgraph Suite de Testes Automática JUnit 5
E[Disparar org.junit.jupiter.api.Test] --> F[Chama BattleEngine.calcularDano20, 10]
F --> G[assertEquals 15, danoCalculado]
G --> H{Dano Calculado == 15?}
H -- Sim --> I[Barra Verde: Teste PASSOU com sucesso!]
H -- Não --> J[Barra Vermelha: Teste FALHOU! Alerta de Bug no código.]
end
📖 Dicionário do Projeto
- Testes Unitários: Códigos de teste escritos para verificar o comportamento isolado de pequenas partes de código (como uma única função ou classe) de forma independente da interface gráfica.
- JUnit 5 (Jupiter): O ecossistema de testes padrão e mais popular do ecossistema Java/Kotlin para a execução automatizada de suítes de validação de software.
- assertEquals(esperado, obtido): O comando de verificação clássico que compara se o valor de resultado calculado pelo seu código condiz exatamente com o valor esperado planejado.
- MockK: A biblioteca de mock padrão de mercado para Kotlin. Ela permite simular o comportamento de classes complexas de banco de dados ou chamadas de internet nas quais o teste unitário não deve tocar de verdade.
🛠️ Passo 1: Configurando as Dependências de Teste (Gradle)
- Abra o arquivo
build.gradle (Module :app)e declare as bibliotecas de testes dentro do bloco de testes unitários:dependencies { // JUnit 5 & MockK para Testes Unitários de Alto Nível testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0" testImplementation "io.mockk:mockk:1.13.8" ... }Clique em Sync Now.
🛠️ Passo 2: Criando a Classe de Testes (BattleEngineTest.kt)
Os testes unitários devem ser criados obrigatoriamente na pasta app > src > test > java > br.com.curso.batalha.
- Crie o arquivo de teste
BattleEngineTest.ktna pasta correspondente do seu projeto: ```kotlin package br.com.curso.batalha
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test
class BattleEngineTest {
@Test
fun `dano comum deve reduzir metade da defesa do inimigo`() {
// Ataque: 20, Defesa: 10 (defesa/2 = 5) -> Dano: 20 - 5 = 15
val danoCalculado = BattleEngine.calcularDano(ataque = 20, defesa = 10)
assertEquals(15, danoCalculado)
}
@Test
fun `dano minimo deve ser sempre 1 mesmo se a defesa for extremamente alta`() {
// Ataque: 5, Defesa: 50 -> Dano bruto seria negativo, deve retornar 1
val danoCalculado = BattleEngine.calcularDano(ataque = 5, defesa = 50)
assertEquals(1, danoCalculado)
}
@Test
fun `ataque elemental contra inimigo vulneravel deve aplicar multiplicador`() {
// Ataque: 10, Defesa: 0, Multiplicador: 2.0f -> Dano: 20
val danoCalculado = BattleEngine.calcularDano(ataque = 10, defesa = 0, multiplicador = 2.0f)
assertEquals(20, danoCalculado)
} } ```
🧠 Passo 3: Executando os Testes Automatizados
No painel do Android Studio, clique com o botão direito sobre a pasta de testes ou sobre o arquivo BattleEngineTest e selecione Run 'BattleEngineTest' (ou toque na seta verde ao lado da declaração do teste).
A barra verde se acenderá no painel inferior confirmando que todas as regras de negócios do seu app de batalhas estão cobertas de bugs e operando em perfeito estado de funcionamento!
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Dano Crítico: implemente 10% de chance de “acerto crítico” (dano ×2) em
calcularDanoe adicione um novo@Testpara validar a regra. - Mais Cenários: adicione
@Tests cobrindo ummultiplicadorfracionário (ex: 0.5f) e o caso dedefesa = 0.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/batalha/MainActivity.kt
package br.com.curso.batalha
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BatalhaTheme {
BatalhaScreen()
}
}
}
}
@Composable
fun BatalhaTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFFFF9100), // Laranja Batalha
background = Color(0xFF0C0C0D),
surface = Color(0xFF1A1A1C)
),
content = content
)
}
// 1. ENGINE MATEMÁTICO DE COMBATE (LÓGICA PURA A SER TESTADA)
object BattleEngine {
fun calcularDano(ataque: Int, defesa: Int, multiplicador: Float = 1.0f): Int {
val danoBruto = ataque - (defesa / 2)
val danoMinimo = if (danoBruto < 1) 1 else danoBruto
return (danoMinimo * multiplicador).toInt()
}
}
// 2. SIMULAÇÃO DE CLASSE DE TESTE UNITÁRIO DIDÁTICO (JUNIT 5)
/*
package br.com.curso.batalha
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class BattleEngineTest {
@Test
fun `dano comum deve reduzir metade da defesa do inimigo`() {
// Ataque: 20, Defesa: 10 (defesa/2 = 5) -> Dano: 20 - 5 = 15
val danoCalculado = BattleEngine.calcularDano(ataque = 20, defesa = 10)
assertEquals(15, danoCalculado)
}
@Test
fun `dano minimo deve ser sempre 1 mesmo se a defesa for extremamente alta`() {
// Ataque: 5, Defesa: 50 -> Dano bruto seria negativo, deve retornar 1
val danoCalculado = BattleEngine.calcularDano(ataque = 5, defesa = 50)
assertEquals(1, danoCalculado)
}
@Test
fun `ataque elemental do tipo fogo contra folha deve aplicar multiplicador duplo`() {
// Ataque: 10, Defesa: 0, Multiplicador: 2.0f -> Dano: 20
val danoCalculado = BattleEngine.calcularDano(ataque = 10, defesa = 0, multiplicador = 2.0f)
assertEquals(20, danoCalculado)
}
}
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BatalhaScreen() {
var atqText by remember { mutableStateOf("20") }
var defText by remember { mutableStateOf("10") }
var resultadoDano by remember { mutableStateOf(15) }
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text("ALLIANCE BATTLE SIMULATOR", fontWeight = FontWeight.Bold, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color(0xFF1A1A1C),
titleContentColor = Color(0xFFFF9100)
)
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFF0C0C0D))
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1C)),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("DANO TOTAL ESTIMADO", fontSize = 12.sp, color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
Text("$resultadoDano HP 💥", fontSize = 48.sp, fontWeight = FontWeight.Black, color = Color(0xFFFF9100))
}
}
Spacer(modifier = Modifier.height(32.dp))
// Inputs
OutlinedTextField(
value = atqText,
onValueChange = { atqText = it },
label = { Text("Poder de Ataque (ATQ)") },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.outlinedTextFieldColors(textColor = Color.White)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = defText,
onValueChange = { defText = it },
label = { Text("Defesa Inimiga (DEF)") },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.outlinedTextFieldColors(textColor = Color.White)
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
val atq = atqText.toIntOrNull() ?: 0
val def = defText.toIntOrNull() ?: 0
resultadoDano = BattleEngine.calcularDano(atq, def)
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9100)),
modifier = Modifier.fillMaxWidth()
) {
Text("EXECUTAR SIMULAÇÃO", color = Color.Black, fontWeight = FontWeight.Bold)
}
}
}
}