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


🎯 Objetivo do Projeto

Aprender a garantir a integridade do código sob testes automatizados contendo:

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


🛠️ Passo 1: Configurando as Dependências de Teste (Gradle)

  1. 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.

  1. Crie o arquivo de teste BattleEngineTest.kt na 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:


📖 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)
            }
        }
    }
}