Skip to main content

Testing Overview

NASA Explorer uses a combination of unit tests and instrumented tests to ensure code quality and reliability. The project follows Android testing best practices with JUnit and Espresso.

Test Structure

The project includes two types of tests:
app/src/
├── test/              # Unit tests (JVM)
│   └── java/com/ccandeladev/nasaexplorer/
│       └── ExampleUnitTest.kt
└── androidTest/       # Instrumented tests (Android device)
    └── java/com/ccandeladev/nasaexplorer/
        └── ExampleInstrumentedTest.kt
Unit tests run on the JVM and are faster, while instrumented tests run on Android devices or emulators and test Android-specific functionality.

Testing Dependencies

The project is configured with the following testing libraries:
testImplementation(libs.junit)  // JUnit 4.13.2

Test Runner Configuration

The app is configured to use the AndroidX test runner:
android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
}

Unit Testing

Unit tests verify business logic without requiring an Android device.

Running Unit Tests

1

Via Android Studio

Right-click on the test directory or a specific test file and select Run Tests.
2

Via Gradle Command

Run all unit tests from the command line:
./gradlew test
Run tests for a specific build variant:
./gradlew testDebugUnitTest
3

View Test Reports

Test reports are generated at:
app/build/reports/tests/testDebugUnitTest/index.html

Example Unit Test

The project includes a basic unit test example:
ExampleUnitTest.kt
package com.ccandeladev.nasaexplorer

import org.junit.Test
import org.junit.Assert.*

class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

Writing Unit Tests

When writing unit tests for NASA Explorer, focus on:
1

ViewModel Logic

Test state management and business logic in ViewModels:
@Test
fun `getDailyImage updates state correctly`() = runTest {
    // Given
    val repository = mockk<NasaRepository>()
    val viewModel = DailyImageViewModel(repository)
    
    // When
    coEvery { repository.getDailyImage() } returns Result.success(mockNasaModel)
    viewModel.loadDailyImage()
    
    // Then
    assertTrue(viewModel.uiState.value is UiState.Success)
}
2

Repository Functions

Test data transformation and API response handling:
@Test
fun `repository maps API response to domain model`() = runTest {
    // Given
    val apiService = mockk<NasaApiService>()
    val repository = NasaRepository(apiService)
    
    // When
    coEvery { apiService.getApod(any(), any()) } returns mockApiResponse
    val result = repository.getDailyImage()
    
    // Then
    assertTrue(result.isSuccess)
    assertEquals("Expected Title", result.getOrNull()?.title)
}
3

Domain Model Validation

Test data model transformations and validation logic:
@Test
fun `NasaModel correctly parses date format`() {
    val model = NasaModel(
        title = "Test",
        date = "2024-03-06",
        // ... other fields
    )
    assertTrue(model.isValidDate())
}

Instrumented Testing

Instrumented tests run on Android devices and test Android-specific functionality.

Running Instrumented Tests

1

Connect Device

Connect an Android device via USB or start an emulator.
2

Via Android Studio

Right-click on the androidTest directory and select Run Tests.
3

Via Gradle Command

Run instrumented tests from the command line:
./gradlew connectedAndroidTest
Run on a specific device:
./gradlew connectedDebugAndroidTest

Example Instrumented Test

ExampleInstrumentedTest.kt
package com.ccandeladev.nasaexplorer

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.ccandeladev.nasaexplorer", appContext.packageName)
    }
}

Testing Compose UI

For testing Jetpack Compose screens, use the Compose testing APIs:
@RunWith(AndroidJUnit4::class)
class LoginScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun loginScreen_displaysEmailAndPasswordFields() {
        composeTestRule.setContent {
            LoginScreen(onLoginSuccess = {})
        }
        
        composeTestRule
            .onNodeWithText("Email")
            .assertIsDisplayed()
        
        composeTestRule
            .onNodeWithText("Password")
            .assertIsDisplayed()
    }
    
    @Test
    fun loginButton_clickable_whenFieldsFilled() {
        composeTestRule.setContent {
            LoginScreen(onLoginSuccess = {})
        }
        
        // Fill in email
        composeTestRule
            .onNodeWithText("Email")
            .performTextInput("[email protected]")
        
        // Fill in password
        composeTestRule
            .onNodeWithText("Password")
            .performTextInput("password123")
        
        // Click login button
        composeTestRule
            .onNodeWithText("Login")
            .performClick()
    }
}

Testing Best Practices

Mocking Dependencies

Use MockK for mocking Kotlin classes:
testImplementation("io.mockk:mockk:1.13.8")
@Test
fun `repository handles API errors gracefully`() = runTest {
    // Given
    val apiService = mockk<NasaApiService>()
    val repository = NasaRepository(apiService)
    
    // When
    coEvery { apiService.getApod(any(), any()) } throws IOException("Network error")
    val result = repository.getDailyImage()
    
    // Then
    assertTrue(result.isFailure)
}

Testing Coroutines

Use runTest from kotlinx-coroutines-test:
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
@Test
fun `viewModel loads data asynchronously`() = runTest {
    val viewModel = MyViewModel(repository)
    viewModel.loadData()
    
    advanceUntilIdle()  // Wait for all coroutines to complete
    
    assertTrue(viewModel.uiState.value is UiState.Success)
}

Testing Firebase

For testing Firebase functionality:
1

Use Firebase Emulator Suite

Run Firebase Authentication and Realtime Database locally:
firebase emulators:start
2

Configure Test Environment

Point your tests to the local emulator:
@Before
fun setUp() {
    FirebaseAuth.getInstance().useEmulator("10.0.2.2", 9099)
    FirebaseDatabase.getInstance().useEmulator("10.0.2.2", 9000)
}
The IP address 10.0.2.2 is used to connect to localhost from an Android emulator. Use localhost for physical devices connected via USB.

Test Coverage

Generate test coverage reports:
# Run tests with coverage
./gradlew testDebugUnitTest jacocoTestReport

# View coverage report
open app/build/reports/jacoco/jacocoTestReport/html/index.html

Coverage Configuration

Add JaCoCo to your build.gradle.kts:
android {
    buildTypes {
        debug {
            enableAndroidTestCoverage = true
            enableUnitTestCoverage = true
        }
    }
}

Continuous Integration

For CI/CD pipelines, run tests in headless mode:
.github/workflows/test.yml
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
      
      - name: Run Unit Tests
        run: ./gradlew test
      
      - name: Run Instrumented Tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 29
          script: ./gradlew connectedCheck
Instrumented tests in CI require an Android emulator, which can significantly increase build times. Consider running them only on pull requests or main branch commits.

Debugging Tests

Tips for debugging failing tests:
1

Enable Verbose Logging

Add detailed logging in your test methods:
@Test
fun myTest() {
    println("Test started")
    val result = performOperation()
    println("Result: $result")
    assertEquals(expected, result)
}
2

Use Debug Breakpoints

Set breakpoints in Android Studio and run tests in debug mode.
3

Check Test Reports

Review detailed error messages in the HTML test reports generated in the build/reports/ directory.

Build docs developers (and LLMs) love