Salta el contingut

Tests Unitaris

Els tests unitaris proven funcions o classes de manera aïllada, sense necessitat del sistema Android. S'executen directament a la JVM del teu ordinador, cosa que els fa molt ràpids.

1. Què són els tests unitaris?

Un test unitari comprova que una unitat de codi (una funció, un mètode o una classe) funciona correctament de manera independent. No necessiten un dispositiu ni un emulador.

  • On van: carpeta app/src/test/java/
  • S'executen a: la JVM local (sense Android)
  • Velocitat: molt ràpids (mil·lisegons)
  • Què proven: lògica de negoci, validacions, càlculs, transformacions de dades

2. Estructura d'un test: patró AAA

Cada test segueix el patró AAA (Arrange-Act-Assert):

@Test
fun sumar_dosNombresPositius_retornaLaSuma() {
    // Arrange — Preparar les dades
    val viewModel = CalculadoraViewModel()

    // Act — Executar l'acció
    viewModel.sumar("10", "5")

    // Assert — Comprovar el resultat
    assertEquals("Resultat: 15.0", viewModel.resultat.value)
}
Fase Què fa Exemple
Arrange Prepara els objectes i dades necessaris Crear instàncies, definir variables
Act Executa la funció que vols provar Cridar el mètode
Assert Comprova que el resultat és l'esperat Usar assertions de JUnit

3. Assertions bàsiques de JUnit

JUnit proporciona diverses funcions per comprovar resultats:

import org.junit.Assert.*

// Comprovar igualtat
assertEquals(esperat, real)

// Comprovar igualtat amb decimals (delta = marge d'error)
assertEquals(3.14, resultat, 0.01)

// Comprovar que és cert
assertTrue(condicio)

// Comprovar que és fals
assertFalse(condicio)

// Comprovar que no és null
assertNotNull(objecte)

// Comprovar que és null
assertNull(objecte)

// Comprovar que llança una excepció
@Test(expected = IllegalArgumentException::class)
fun test_llancaExcepcio() {
    // codi que ha de llançar l'excepció
}

assertEquals amb decimals

Quan compares nombres Double, afegeix sempre un tercer paràmetre delta (marge d'error), perquè els decimals en punt flotant poden tenir petites imprecisions:

assertEquals(3.14, resultat, 0.001)

4. Testejar un ViewModel amb LiveData

Quan el ViewModel utilitza LiveData, cal una configuració especial perquè LiveData necessita el thread principal d'Android, que no existeix als tests unitaris. La solució és afegir una Rule que fa que LiveData funcioni de manera síncrona.

Dependència necessària

Al fitxer build.gradle.kts del mòdul app, afegeix:

dependencies {
    // Tests unitaris
    testImplementation("junit:junit:4.13.2")
    testImplementation("androidx.arch.core:core-testing:2.2.0")
}

InstantTaskExecutorRule

Aquesta Rule fa que les operacions de LiveData s'executin immediatament en el mateix thread, en lloc de necessitar el thread principal d'Android:

@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

Sense aquesta Rule, els tests fallen!

Si intentes observar un LiveData en un test unitari sense InstantTaskExecutorRule, obtindràs un error com:

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.

5. Exemple pràctic: CalculadoraViewModel

Crearem un CalculadoraViewModel que suma dos nombres i exposa el resultat i els errors mitjançant LiveData.

Primer: els tests (TDD)

Fitxer app/src/test/java/com/example/app/CalculadoraViewModelTest.kt:

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class CalculadoraViewModelTest {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: CalculadoraViewModel

    @Before
    fun setUp() {
        viewModel = CalculadoraViewModel()
    }

    // --- sumar amb valors vàlids ---

    @Test
    fun sumar_dosNombresPositius_mostraResultat() {
        viewModel.sumar("10", "5")

        assertEquals("Resultat: 15.0", viewModel.resultat.value)
    }

    @Test
    fun sumar_ambDecimals_mostraResultat() {
        viewModel.sumar("3.5", "2.5")

        assertEquals("Resultat: 6.0", viewModel.resultat.value)
    }

    @Test
    fun sumar_ambNegatius_mostraResultat() {
        viewModel.sumar("-3", "8")

        assertEquals("Resultat: 5.0", viewModel.resultat.value)
    }

    @Test
    fun sumar_ambZeros_mostraResultat() {
        viewModel.sumar("0", "0")

        assertEquals("Resultat: 0.0", viewModel.resultat.value)
    }

    // --- sumar amb camps buits ---

    @Test
    fun sumar_ambPrimerCampBuit_mostraError() {
        viewModel.sumar("", "5")

        assertEquals("Has d'omplir els dos camps", viewModel.error.value)
    }

    @Test
    fun sumar_ambSegonCampBuit_mostraError() {
        viewModel.sumar("10", "")

        assertEquals("Has d'omplir els dos camps", viewModel.error.value)
    }

    @Test
    fun sumar_ambDosCampsBuits_mostraError() {
        viewModel.sumar("", "")

        assertEquals("Has d'omplir els dos camps", viewModel.error.value)
    }

    // --- sumar amb text invàlid ---

    @Test
    fun sumar_ambTextAlPrimerCamp_mostraError() {
        viewModel.sumar("abc", "5")

        assertEquals("Introdueix nombres vàlids", viewModel.error.value)
    }

    @Test
    fun sumar_ambTextAlSegonCamp_mostraError() {
        viewModel.sumar("5", "xyz")

        assertEquals("Introdueix nombres vàlids", viewModel.error.value)
    }

    // --- comportament de neteja ---

    @Test
    fun sumar_despresError_netejaError() {
        // Primer provoquem un error
        viewModel.sumar("", "")
        assertEquals("Has d'omplir els dos camps", viewModel.error.value)

        // Després fem una suma vàlida
        viewModel.sumar("3", "7")

        assertEquals("Resultat: 10.0", viewModel.resultat.value)
        assertNull(viewModel.error.value)
    }

    @Test
    fun sumar_despresResultat_netejaResultat() {
        // Primer fem una suma vàlida
        viewModel.sumar("3", "7")
        assertEquals("Resultat: 10.0", viewModel.resultat.value)

        // Després provoquem un error
        viewModel.sumar("abc", "5")

        assertEquals("Introdueix nombres vàlids", viewModel.error.value)
        assertEquals("", viewModel.resultat.value)
    }
}

Després: la implementació

Fitxer app/src/main/java/com/example/app/CalculadoraViewModel.kt:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class CalculadoraViewModel : ViewModel() {

    private val _resultat = MutableLiveData<String>()
    val resultat: LiveData<String> = _resultat

    private val _error = MutableLiveData<String?>()
    val error: LiveData<String?> = _error

    fun sumar(text1: String, text2: String) {
        if (text1.isBlank() || text2.isBlank()) {
            _error.value = "Has d'omplir els dos camps"
            _resultat.value = ""
            return
        }

        val n1: Double? = text1.toDoubleOrNull()
        val n2: Double? = text2.toDoubleOrNull()

        if (n1 == null || n2 == null) {
            _error.value = "Introdueix nombres vàlids"
            _resultat.value = ""
            return
        }

        _error.value = null
        _resultat.value = "Resultat: ${n1 + n2}"
    }
}

6. Organització amb @Before i @After

JUnit proporciona anotacions per executar codi abans i després de cada test:

class CalculadoraViewModelTest {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: CalculadoraViewModel

    @Before
    fun setUp() {
        // S'executa ABANS de cada test
        // Cada test obté un ViewModel nou i net
        viewModel = CalculadoraViewModel()
    }

    @After
    fun tearDown() {
        // S'executa DESPRÉS de cada test
        // Ideal per netejar recursos (fitxers, connexions, etc.)
    }

    @Test
    fun elMeuTest() {
        // Aquí viewModel ja està inicialitzat
    }
}

Per què @Before?

Usant @Before, cada test obté una instància nova del ViewModel. Això garanteix que els tests són independents: un test no afecta els altres.

7. Executar tests des d'Android Studio

Hi ha diverses maneres d'executar els tests unitaris:

Des de l'editor

  • Fes clic a la fletxa verda que apareix al costat del nom del test o de la classe.
  • Selecciona Run 'NomDelTest'.

Des del menú

  • Run → Run... i selecciona la classe de test.

Des del terminal

./gradlew test

Aquesta comanda executa tots els tests unitaris del projecte.

Resultats dels tests

Android Studio mostra els resultats en una finestra amb:

  • Tests en verd: han passat correctament.
  • Tests en vermell: han fallat — fes clic per veure el detall de l'error.