====== Estrategia de Testing en PAE ======
===== Pirámide de testing =====
/\
/ \ E2E Tests
/ \
/------\
/ \ Integration Tests
/ \
/ \
/ \
/----------------\
/ \ Unit Tests
/____________________|
==== Distribución recomendada ====
* **Unit Tests**: 70% (rápidos, aislados)
* **Integration Tests**: 20% (módulos, servicios)
* **E2E Tests**: 10% (flujos completos)
===== Unit Tests =====
==== Estructura ====
Machine/src/
├── test/
│ └── java/co/ada/paemachine/
│ ├── viewmodels/
│ │ └── LaboratoryViewModelTest.kt
│ ├── state/
│ │ └── StateManagerTest.kt
│ └── services/
│ └── DeliveryServiceTest.kt
==== Ejemplo: StateManager ====
// MachineDomain/src/test/kotlin/co/ada/domain/state/StateManagerTest.kt
@RunWith(RobolectricTestRunner::class)
class StateManagerTest {
private lateinit var manager: StateManager
private val mockRepository: Repository = mock()
private val mockHardware: Hardware = mock()
@Before
fun setup() {
manager = StateManager(mockRepository, mockHardware)
}
@Test
fun testStateTransitionWaitingForWeight() {
// Arrange
val context = RuntimeEnvironment.getApplication()
// Act
val initialized = manager.init(context)
val currentState = manager.getCurrentState()
// Assert
assertTrue(initialized)
assertEquals("WaitingForWeight", currentState)
}
@Test
fun testStateTransitionOnSuccess() {
// Arrange
val results = mutableListOf()
manager.init(RuntimeEnvironment.getApplication())
// Act
manager.cycle()
results.add(manager.getCurrentState())
// Assert
assertTrue(results.isNotEmpty())
}
@Test
fun testStateRetryOnError() {
// Arrange
val manager = StateManager(mockRepository, mockHardware)
// Act
manager.init(RuntimeEnvironment.getApplication())
// TODO: simular error y verificar retry
// Assert
}
}
==== Ejemplo: BeneficiaryService ====
// MachineData/src/test/kotlin/co/ada/data/services/BeneficiaryServiceTest.kt
class BeneficiaryServiceTest {
private val mockRepository: Repository = mock()
private lateinit var service: BeneficiaryService
@Before
fun setup() {
service = BeneficiaryService(mockRepository)
}
@Test
fun testFindSimilar() {
// Arrange
val embedding = FloatArray(512) { Random.nextFloat() }
val beneficiaries = listOf(
Beneficiary(id = 1, name = "Juan", embedding = embedding),
Beneficiary(id = 2, name = "María", embedding = embedding)
)
`when`(mockRepository.beneficiaries.nearestNeighbors(embedding, 5))
.thenReturn(beneficiaries)
// Act
val results = service.findSimilar(embedding)
// Assert
assertEquals(2, results.size)
verify(mockRepository.beneficiaries).nearestNeighbors(embedding, 5)
}
@Test
fun testCreateBeneficiary() {
// Arrange
val beneficiary = Beneficiary(
id = 1,
name = "Juan",
embedding = FloatArray(512)
)
// Act
service.create(beneficiary)
// Assert
verify(mockRepository.beneficiaries).create(beneficiary)
}
}
==== Test fixtures ====
// shared/src/test/kotlin/co/ada/test/fixtures/
object BeneficiaryFixtures {
fun makeBeneficiary(
id: Long = 1,
name: String = "Juan",
embedding: FloatArray = FloatArray(512)
) = Beneficiary(
id = id,
name = name,
embedding = embedding
)
}
object DeliveryFixtures {
fun makeDelivery(
id: Long = 1,
weight: Float = 10.5f,
similarity: Float = 0.95f
) = Delivery(
id = id,
weight = weight,
similarity = similarity
)
}
===== Android Tests (Instrumented) =====
==== Estructura ====
Machine/src/
├── androidTest/
│ └── java/co/ada/paemachine/
│ ├── screens/
│ │ └── LaboratoryScreenTest.kt
│ └── integration/
│ └── DeliveryFlowTest.kt
==== Ejemplo: Composable UI Test ====
// Machine/src/androidTest/kotlin/co/ada/paemachine/screens/LaboratoryScreenTest.kt
@RunWith(AndroidJUnit4::class)
class LaboratoryScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testCapturButtonVisibile() {
// Arrange
composeTestRule.setContent {
LaboratoryScreen(viewModel = mockViewModel)
}
// Act & Assert
composeTestRule.onNodeWithText("Capturar").assertIsDisplayed()
}
@Test
fun testCapturButtonClick() {
// Arrange
composeTestRule.setContent {
LaboratoryScreen(viewModel = mockViewModel)
}
// Act
composeTestRule.onNodeWithText("Capturar").performClick()
// Assert
verify(mockViewModel).startCapture()
}
@Test
fun testStateProgression() {
// Arrange
composeTestRule.setContent {
LaboratoryScreen(viewModel = mockViewModel)
}
// Act
composeTestRule.onNodeWithText("Capturar").performClick()
composeTestRule.waitUntil(5000) {
// esperar estado cambio
true
}
// Assert
composeTestRule.onNodeWithText("Capturando rostro").assertIsDisplayed()
}
}
==== Database Test ====
// MachineData/src/androidTest/kotlin/co/ada/data/DeliveryDatabaseTest.kt
@RunWith(AndroidJUnit4::class)
class DeliveryDatabaseTest {
private lateinit var db: DeliveryDatabase
private lateinit var dao: DeliveryDao
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, DeliveryDatabase::class.java)
.allowMainThreadQueries()
.build()
dao = db.deliveryDao()
}
@After
fun closeDb() {
db.close()
}
@Test
fun testInsertAndGetDelivery() = runBlocking {
// Arrange
val delivery = Delivery(
id = 1,
weight = 10.5f,
similarity = 0.95f
)
// Act
dao.insert(delivery)
val retrieved = dao.getById(1)
// Assert
assertNotNull(retrieved)
assertEquals(10.5f, retrieved?.weight)
}
}
===== Integration Tests =====
==== Ejemplo: DeliverySyncWorker ====
// RutaPAE/src/androidTest/kotlin/co/ada/rutapae/workers/DeliverySyncWorkerTest.kt
@RunWith(AndroidJUnit4::class)
class DeliverySyncWorkerTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var context: Context
private lateinit var testDriver: WorkManagerTestInitHelper
@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
testDriver = WorkManagerTestInitHelper(context)
}
@Test
fun testSyncWorkerSuccess() {
// Arrange
val machineId = 1L
val request = OneTimeWorkRequestBuilder()
.setInputData(
workDataOf("machineId" to machineId)
)
.build()
// Act
WorkManager.getInstance(context).enqueueUniqueWork(
"sync_$machineId",
ExistingWorkPolicy.KEEP,
request
)
testDriver.periodicallyEnqueue(request.id)
testDriver.work()
// Assert
testDriver.getWorkInfoManager().getWorkInfoById(request.id).get()
.also { info ->
assertEquals(WorkInfo.State.SUCCEEDED, info.state)
}
}
}
===== E2E Tests (Espresso) =====
==== Ejemplo: Flujo completo ====
// Machine/src/androidTest/kotlin/co/ada/paemachine/DeliveryFlowTest.kt
@RunWith(AndroidJUnit4::class)
class DeliveryFlowTest {
@get:Rule
val activityRule = ActivityScenarioRule(MachineActivity::class.java)
@Test
fun testCompleteDeliveryFlow() {
// 1. Navega a Laboratory Screen
onView(withId(R.id.laboratory_screen)).check(matches(isDisplayed()))
// 2. Presiona Capturar
onView(withText("Capturar")).perform(click())
// 3. Espera a WaitingForWeight
onView(withText("Esperando peso..."))
.check(matches(isDisplayed()))
// 4. Simula input de balanza
// (requiere mock de Hardware)
// 5. Verifica transición a CaptureImages
onView(withText("Capturando imagen..."))
.check(matches(isDisplayed()))
// 6. Verifica progreso completo
onView(withText("Entrega guardada"))
.check(matches(isDisplayed()))
}
}
===== Mocking y Testing Doubles =====
==== Mockito ====
// Crear mock
val mockRepository: Repository = mock()
// Configurar comportamiento
`when`(mockRepository.getDelivery(1)).thenReturn(DeliveryFixtures.makeDelivery())
// Verificar llamadas
verify(mockRepository, times(1)).getDelivery(1)
verify(mockRepository, never()).deleteDelivery(anyLong())
==== Manual Test Doubles ====
class FakeRepository : Repository {
private val deliveries = mutableMapOf()
override fun getDelivery(id: Long): Delivery? = deliveries[id]
override fun create(delivery: Delivery): Delivery {
deliveries[delivery.id] = delivery
return delivery
}
override fun delete(id: Long): Boolean {
return deliveries.remove(id) != null
}
}
class StubHardware : Hardware {
override fun getWeight(): Float = 10.5f
override fun ledOn() { /* no-op */ }
}
===== Coverage de código =====
==== Ejecutar coverage ====
./gradlew jacocoTestReport
# Report en build/reports/jacoco/
==== Configurar jacoco ====
// build.gradle.kts
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.8"
}
task jacocoTestReport(type: JacocoReport) {
dependsOn test
reports {
xml.enabled true
html.enabled true
}
}
==== Mínimo coverage requerido ====
* **Domain**: 80%+
* **Data**: 70%+
* **UI**: 50%
===== Continuous Integration Testing =====
==== GitHub Actions ====
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run unit tests
run: ./gradlew test
- name: Generate coverage
run: ./gradlew jacocoTestReport
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./build/reports/jacoco/
===== Performance Testing =====
==== Memory profiler ====
# Conectar dispositivo
adb shell am start-profiler [process] [file]
# Capturar dump
adb shell am dump-heap [process] [file]
==== CPU profiler ====
# Android Studio: Profiler → CPU
# o vía Logcat: adb logcat | grep "Trace"
===== Debugging de tests =====
==== Breakpoints en tests ====
* Android Studio: Click en número de línea
* Run → Debug 'TestName'
* F9 para continuar
==== Logs en tests ====
@Test
fun myTest() {
android.util.Log.d("TEST", "Starting test")
// ...
}
Ver logs:
adb logcat | grep TEST
===== Best practices de testing =====
- **AAA Pattern**: Arrange, Act, Assert
- **One assertion per test**: facilita debugging
- **Descriptive names**: ''testWeightInputUpdatesUI'' not ''test1''
- **Isolate dependencies**: usar mocks
- **Mock external systems**: HTTP, BD, hardware
- **Fast tests**: < 100ms para unit tests
- **Deterministic**: mismo resultado siempre
- **Independientes**: no dependen del orden
- **Limpieza**: reset mocks y estado en @After
===== Test Data Builders =====
class DeliveryBuilder {
private var id: Long = 1
private var weight: Float = 10.5f
private var similarity: Float = 0.95f
fun withId(id: Long) = apply { this.id = id }
fun withWeight(weight: Float) = apply { this.weight = weight }
fun withSimilarity(similarity: Float) = apply { this.similarity = similarity }
fun build() = Delivery(
id = id,
weight = weight,
similarity = similarity
)
}
// Uso
val delivery = DeliveryBuilder()
.withId(2)
.withWeight(15f)
.build()
===== Parametrized Tests =====
@RunWith(Parameterized::class)
class WeightValidationTest(
val input: Float,
val expected: Boolean
) {
companion object {
@Parameterized.Parameters
@JvmStatic
fun data() = listOf(
arrayOf(0f, false), // peso 0 es inválido
arrayOf(5.5f, true), // peso válido
arrayOf(100f, true), // peso válido
arrayOf(-1f, false) // peso negativo inválido
)
}
@Test
fun testWeightValidation() {
val result = WeightValidator.isValid(input)
assertEquals(expected, result)
}
}