Skip to main content
Nimaz uses a comprehensive testing strategy combining unit tests, instrumented tests, and UI tests to ensure reliability and quality.

Testing overview

Test types

Unit tests (src/test/)
  • Fast, isolated tests running on the JVM
  • Test business logic, ViewModels, repositories, and utilities
  • Run without Android framework dependencies
  • Frameworks: JUnit, MockK, Turbine, Coroutines Test, Truth
Instrumented tests (src/androidTest/)
  • Tests running on Android devices or emulators
  • Test Android framework interactions, database, UI components
  • Frameworks: AndroidJUnit, Espresso, Compose UI Test, Hilt Testing
Lint checks
  • Static code analysis
  • Detects potential bugs, performance issues, and style violations

Running tests

Using Android Studio

1

Run all unit tests

Right-click on app/src/test/Run ‘Tests in test’
2

Run specific test class

Open the test file → Click the green play button in the gutter → Run
3

Run single test method

Click the play button next to the test method → Run
4

Run with coverage

Right-click test → Run with Coverage to see code coverage metrics

Using Gradle

Unit tests

# Run all unit tests
./gradlew test

# Run unit tests with detailed output
./gradlew test --info

# Run tests for specific build variant
./gradlew testDebugUnitTest
./gradlew testReleaseUnitTest

# Run specific test class
./gradlew test --tests="com.arshadshah.nimaz.ExampleUnitTest"

# Run tests matching pattern
./gradlew test --tests="*Repository*"
Test reports location:
app/build/reports/tests/testDebugUnitTest/index.html

Instrumented tests

Instrumented tests require a connected Android device or running emulator.
# Start an emulator first, then:
./gradlew connectedAndroidTest

# Run on specific device (if multiple connected)
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.arshadshah.nimaz.ExampleInstrumentedTest

# Run specific test class
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.arshadshah.nimaz.database.PrayerDaoTest
Test reports location:
app/build/reports/androidTests/connected/index.html

Lint checks

# Run lint analysis
./gradlew lint

# Run lint and fail build on errors
./gradlew lintDebug

# Generate lint report
./gradlew lintRelease
Lint reports location:
app/build/reports/lint-results-debug.html
app/build/reports/lint-results-release.html

Using Fastlane

# Run tests and lint together
bundle exec fastlane android test

# Run only lint
bundle exec fastlane android lint
This is the same command used in CI/CD pipelines.

Writing tests

Unit test structure

package com.arshadshah.nimaz.domain

import com.google.common.truth.Truth.assertThat
import io.mockk.mockk
import io.mockk.coEvery
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test

class PrayerTimeCalculatorTest {
    
    private lateinit var calculator: PrayerTimeCalculator
    private lateinit var locationProvider: LocationProvider
    
    @Before
    fun setup() {
        locationProvider = mockk()
        calculator = PrayerTimeCalculator(locationProvider)
    }
    
    @Test
    fun `calculate prayer times returns five prayers`() = runTest {
        // Given
        val coordinates = Coordinates(latitude = 51.5074, longitude = -0.1278)
        val date = LocalDate(2024, 1, 15)
        
        coEvery { locationProvider.getCurrentLocation() } returns coordinates
        
        // When
        val prayerTimes = calculator.calculatePrayerTimes(date)
        
        // Then
        assertThat(prayerTimes).hasSize(5)
        assertThat(prayerTimes.map { it.name }).containsExactly(
            "Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"
        )
    }
    
    @Test
    fun `prayer times are in chronological order`() = runTest {
        // Given
        val date = LocalDate(2024, 1, 15)
        
        // When
        val prayerTimes = calculator.calculatePrayerTimes(date)
        
        // Then
        val times = prayerTimes.map { it.time }
        assertThat(times).isInOrder()
    }
}

Testing coroutines and Flows

Nimaz uses kotlinx-coroutines-test and Turbine for testing async code:
import app.cash.turbine.test
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test

class PrayerRepositoryTest {
    
    private val testDispatcher = StandardTestDispatcher()
    
    @Test
    fun `repository emits prayer times when available`() = runTest(testDispatcher) {
        // Given
        val repository = PrayerRepository(dao, preferences)
        
        // When/Then
        repository.getPrayerTimes().test {
            val emission = awaitItem()
            assertThat(emission).isNotEmpty()
            awaitComplete()
        }
    }
}

Testing ViewModels

import androidx.lifecycle.SavedStateHandle
import com.google.common.truth.Truth.assertThat
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class PrayerTimesViewModelTest {
    
    private val testDispatcher = StandardTestDispatcher()
    private lateinit var viewModel: PrayerTimesViewModel
    private lateinit var repository: PrayerRepository
    
    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        repository = mockk(relaxed = true)
        viewModel = PrayerTimesViewModel(repository, SavedStateHandle())
    }
    
    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }
    
    @Test
    fun `initial state is loading`() {
        assertThat(viewModel.uiState.value).isInstanceOf(UiState.Loading::class.java)
    }
    
    @Test
    fun `load prayer times updates state to success`() = runTest(testDispatcher) {
        // Given
        val expectedTimes = listOf(/* ... */)
        coEvery { repository.getPrayerTimes() } returns flowOf(expectedTimes)
        
        // When
        viewModel.loadPrayerTimes()
        testDispatcher.scheduler.advanceUntilIdle()
        
        // Then
        val state = viewModel.uiState.value
        assertThat(state).isInstanceOf(UiState.Success::class.java)
        assertThat((state as UiState.Success).data).isEqualTo(expectedTimes)
    }
}

Instrumented tests

Testing database (Room)

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PrayerDaoTest {
    
    private lateinit var database: NimazDatabase
    private lateinit var prayerDao: PrayerDao
    
    @Before
    fun setup() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        database = Room.inMemoryDatabaseBuilder(
            context,
            NimazDatabase::class.java
        ).build()
        prayerDao = database.prayerDao()
    }
    
    @After
    fun tearDown() {
        database.close()
    }
    
    @Test
    fun insertAndRetrievePrayerTime() = runTest {
        // Given
        val prayerTime = PrayerTimeEntity(
            id = 1,
            name = "Fajr",
            time = "05:30",
            date = "2024-01-15"
        )
        
        // When
        prayerDao.insert(prayerTime)
        val retrieved = prayerDao.getPrayerTimeById(1).first()
        
        // Then
        assertThat(retrieved).isEqualTo(prayerTime)
    }
}

Testing Compose UI

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PrayerTimesScreenTest {
    
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun prayerTimesScreen_displaysAllPrayers() {
        // Given
        val prayerTimes = listOf(
            PrayerTime("Fajr", "05:30"),
            PrayerTime("Dhuhr", "12:45"),
            // ...
        )
        
        // When
        composeTestRule.setContent {
            PrayerTimesScreen(prayerTimes = prayerTimes)
        }
        
        // Then
        composeTestRule.onNodeWithText("Fajr").assertIsDisplayed()
        composeTestRule.onNodeWithText("05:30").assertIsDisplayed()
    }
    
    @Test
    fun clickingPrayer_showsDetails() {
        // Given
        composeTestRule.setContent {
            PrayerTimesScreen(prayerTimes = testPrayerTimes)
        }
        
        // When
        composeTestRule.onNodeWithText("Fajr").performClick()
        
        // Then
        composeTestRule.onNodeWithText("Prayer Details").assertIsDisplayed()
    }
}

Testing with Hilt

For tests requiring dependency injection:
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject

@HiltAndroidTest
@UninstallModules(ProductionModule::class)
class HiltIntegrationTest {
    
    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    
    @Inject
    lateinit var repository: PrayerRepository
    
    @Before
    fun setup() {
        hiltRule.inject()
    }
    
    @Test
    fun testWithInjectedDependencies() {
        // Test using injected repository
    }
}

Test dependencies

Nimaz includes these testing libraries (see app/build.gradle.kts:143-166):

Unit testing

  • JUnit: Test framework
  • MockK: Mocking library for Kotlin
  • Turbine: Flow testing library
  • Kotlinx Coroutines Test: Coroutine testing utilities
  • Google Truth: Fluent assertion library
  • Robolectric: Android framework simulation on JVM

Instrumented testing

  • AndroidJUnit: Android test runner
  • Espresso: UI testing framework
  • Compose UI Test: Jetpack Compose testing
  • MockK Android: Android-specific mocking
  • Hilt Testing: Dependency injection testing
  • Room Testing: Database testing utilities

CI/CD testing

GitHub Actions automatically runs tests on pull requests:
# .github/workflows/pr_checks.yml
- name: Run Tests and Lint
  run: bundle exec fastlane android test

- name: Upload Test Results
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: app/build/reports/tests/
All PRs must pass tests before merging.

Best practices

Test naming

Use descriptive test names that explain the scenario:
// Good
@Test
fun `calculatePrayerTimes returns empty list when location is unavailable`()

@Test
fun `repository emits updated prayer times when database changes`()

// Avoid
@Test
fun test1()

@Test
fun testPrayerTimes()

Test structure

Follow the Given-When-Then pattern:
@Test
fun testExample() {
    // Given - Set up test conditions
    val input = "test data"
    
    // When - Execute the action being tested
    val result = functionUnderTest(input)
    
    // Then - Verify the outcome
    assertThat(result).isEqualTo(expectedValue)
}

Test isolation

Each test should be independent:
@Before
fun setup() {
    // Fresh setup for each test
}

@After
fun tearDown() {
    // Clean up after each test
}

Mock external dependencies

Mock network calls, location services, and other external dependencies:
val mockLocationProvider = mockk<LocationProvider>()
coEvery { mockLocationProvider.getCurrentLocation() } returns testCoordinates
Use real implementations for Room database tests (in-memory database) but mock network and external services.

Coverage goals

While Nimaz doesn’t enforce strict coverage percentages, aim for:
  • ViewModels: 80%+ coverage
  • Repositories: 70%+ coverage
  • Business logic: 80%+ coverage
  • Utilities: 90%+ coverage
  • UI components: Focus on critical user flows

Troubleshooting

Tests fail with “No such file or directory”

Ensure Room schema export directory exists:
mkdir -p app/schemas

Instrumented tests timeout

Increase test timeout:
@get:Rule
val timeout = Timeout.seconds(30)

Hilt injection fails in tests

Ensure you’re using @HiltAndroidTest and calling hiltRule.inject():
@Before
fun setup() {
    hiltRule.inject() // Must be called before accessing injected fields
}

Compose tests can’t find nodes

Use composeTestRule.waitForIdle() or waitUntil:
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Expected Text").assertIsDisplayed()

Next steps

Build docs developers (and LLMs) love