/\
/ \ E2E Tests
/ \
/------\
/ \ Integration Tests
/ \
/ \
/ \
/----------------\
/ \ Unit Tests
/____________________|
Machine/src/ ├── test/ │ └── java/co/ada/paemachine/ │ ├── viewmodels/ │ │ └── LaboratoryViewModelTest.kt │ ├── state/ │ │ └── StateManagerTest.kt │ └── services/ │ └── DeliveryServiceTest.kt
// 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<String>() 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 } }
// 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) } }
// 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 ) }
Machine/src/ ├── androidTest/ │ └── java/co/ada/paemachine/ │ ├── screens/ │ │ └── LaboratoryScreenTest.kt │ └── integration/ │ └── DeliveryFlowTest.kt
// 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() } }
// 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) } }
// 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<DeliverySyncWorker>() .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) } } }
// 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())) } }
// 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())
class FakeRepository : Repository { private val deliveries = mutableMapOf<Long, Delivery>() 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 */ } }
./gradlew jacocoTestReport # Report en build/reports/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 } }
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/
# Conectar dispositivo adb shell am start-profiler [process] [file] # Capturar dump adb shell am dump-heap [process] [file]
# Android Studio: Profiler → CPU # o vía Logcat: adb logcat | grep "Trace"
@Test fun myTest() { android.util.Log.d("TEST", "Starting test") // ... }
Ver logs:
adb logcat | grep TEST
testWeightInputUpdatesUI not test1class 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()
@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) } }