Skip to main content
This guide covers testing strategies, practices, and tools for ensuring Divvy’s quality and reliability.

Testing Overview

Divvy’s test suite is designed to cover:
  • Unit tests - Repository logic, ViewModels, and business logic
  • UI tests - Compose screen interactions and user flows
  • Integration tests - End-to-end feature workflows
As of the current codebase snapshot, formal test files are not yet implemented. This guide outlines the recommended testing strategy and setup for contributors.

Testing Stack

Divvy uses the following testing libraries configured in app/build.gradle.kts:1:
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)

Key Dependencies

LibraryPurpose
JUnitUnit testing framework
AndroidX TestAndroid instrumented tests
EspressoUI interaction testing
Compose TestJetpack Compose UI testing

Project Test Structure

Tests should be organized in the following directories:
app/src/
├── test/java/com/example/divvy/          # Unit tests
│   ├── backend/                          # Repository tests
│   ├── ui/                               # ViewModel tests
│   └── models/                           # Model validation tests
└── androidTest/java/com/example/divvy/   # Instrumented tests
    ├── ui/                               # Compose UI tests
    └── integration/                      # End-to-end tests

Unit Testing

Testing Repositories

Repositories interact with Supabase and should be tested with mocked dependencies.
1

Create repository test file

Add a test file in app/src/test/java/com/example/divvy/backend/:
// app/src/test/java/com/example/divvy/backend/GroupRepositoryTest.kt
package com.example.divvy.backend

import com.example.divvy.models.Group
import io.github.jan.supabase.SupabaseClient
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.Assert.*
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

class GroupRepositoryTest {

    @Mock
    private lateinit var supabaseClient: SupabaseClient

    private lateinit var repository: SupabaseGroupRepository

    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        repository = SupabaseGroupRepository(supabaseClient)
    }

    @Test
    fun `listGroups returns success with data`() = runTest {
        // Arrange
        val mockGroups = listOf(
            Group(id = "1", name = "Test Group", balanceCents = 0)
        )
        // Mock supabase response

        // Act
        val result = repository.listGroups().first()

        // Assert
        assertTrue(result is DataResult.Success)
        assertEquals(mockGroups, (result as DataResult.Success).data)
    }
}
2

Test error handling

Ensure repositories handle errors gracefully:
@Test
fun `listGroups returns error on network failure`() = runTest {
    // Arrange
    whenever(supabaseClient.from("groups"))
        .thenThrow(RuntimeException("Network error"))

    // Act
    val result = repository.listGroups().first()

    // Assert
    assertTrue(result is DataResult.Error)
    assertEquals("Network error", (result as DataResult.Error).message)
}

Testing ViewModels

ViewModels manage UI state and should be tested for correct state transitions.
1

Create ViewModel test

Add test in app/src/test/java/com/example/divvy/ui/home/:
// app/src/test/java/com/example/divvy/ui/home/HomeViewModelTest.kt
package com.example.divvy.ui.home

import com.example.divvy.backend.GroupRepository
import com.example.divvy.backend.DataResult
import com.example.divvy.models.Group
import com.example.divvy.ui.home.ViewModels.HomeViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.Assert.*
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {

    @Mock
    private lateinit var groupRepository: GroupRepository

    private lateinit var viewModel: HomeViewModel

    private val testDispatcher = StandardTestDispatcher()

    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `uiState shows loading initially`() = runTest {
        // Arrange
        whenever(groupRepository.listGroups())
            .thenReturn(flowOf(DataResult.Loading))

        // Act
        viewModel = HomeViewModel(groupRepository)

        // Assert
        assertTrue(viewModel.uiState.value.isLoading)
    }

    @Test
    fun `uiState shows groups on success`() = runTest {
        // Arrange
        val groups = listOf(Group(id = "1", name = "Test", balanceCents = 100))
        whenever(groupRepository.listGroups())
            .thenReturn(flowOf(DataResult.Success(groups)))

        // Act
        viewModel = HomeViewModel(groupRepository)
        advanceUntilIdle()

        // Assert
        assertFalse(viewModel.uiState.value.isLoading)
        assertEquals(groups, viewModel.uiState.value.groups)
    }
}

UI Testing with Compose Test

Compose UI tests verify screen rendering and user interactions.
1

Create UI test file

Add test in app/src/androidTest/java/com/example/divvy/ui/:
// app/src/androidTest/java/com/example/divvy/ui/home/HomeScreenTest.kt
package com.example.divvy.ui.home

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import com.example.divvy.models.Group
import com.example.divvy.ui.home.Views.HomeScreen
import com.example.divvy.ui.theme.DivvyTheme
import org.junit.Rule
import org.junit.Test

class HomeScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun homeScreen_displaysGroupList() {
        // Arrange
        val groups = listOf(
            Group(id = "1", name = "Roommates", balanceCents = 5000),
            Group(id = "2", name = "Trip to NYC", balanceCents = -2000)
        )

        // Act
        composeTestRule.setContent {
            DivvyTheme {
                // Render HomeScreen with test data
            }
        }

        // Assert
        composeTestRule.onNodeWithText("Roommates").assertIsDisplayed()
        composeTestRule.onNodeWithText("Trip to NYC").assertIsDisplayed()
    }

    @Test
    fun homeScreen_clickGroup_navigatesToDetail() {
        var clickedGroupId: String? = null

        composeTestRule.setContent {
            DivvyTheme {
                // HomeScreen with onGroupClick = { clickedGroupId = it }
            }
        }

        // Act
        composeTestRule.onNodeWithText("Roommates").performClick()

        // Assert
        assert(clickedGroupId == "1")
    }
}
2

Test user flows

Verify multi-step interactions:
@Test
fun createGroup_flow_success() {
    composeTestRule.setContent {
        DivvyTheme { GroupsScreen() }
    }

    // Click create button
    composeTestRule.onNodeWithText("Create Group").performClick()

    // Enter group name
    composeTestRule.onNodeWithText("Group Name")
        .performTextInput("New Group")

    // Submit
    composeTestRule.onNodeWithText("Create").performClick()

    // Verify success
    composeTestRule.onNodeWithText("New Group").assertIsDisplayed()
}

Compose Test Semantics

Use semantic properties for testability:
@Composable
fun GroupItem(group: Group, onClick: () -> Unit) {
    Card(
        onClick = onClick,
        modifier = Modifier.testTag("group_item_${group.id}")
    ) {
        Text(
            text = group.name,
            modifier = Modifier.semantics { contentDescription = "Group name" }
        )
    }
}

// Test
composeTestRule.onNodeWithTag("group_item_1").performClick()

Running Tests

Run All Unit Tests

./gradlew test
This runs tests in app/src/test/ and outputs results to:
app/build/reports/tests/testDebugUnitTest/index.html

Run All Instrumented Tests

Requires a connected device or emulator:
./gradlew connectedAndroidTest
Results are saved to:
app/build/reports/androidTests/connected/index.html

Run Specific Test Class

# Unit test
./gradlew test --tests HomeViewModelTest

# Instrumented test
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.divvy.ui.home.HomeScreenTest

Run Tests in Android Studio

  1. Right-click on a test file or directory
  2. Select Run ‘Tests in …’
  3. View results in the Run tool window
Instrumented tests run on physical devices or emulators, while unit tests run on the JVM. Use unit tests for fast feedback and instrumented tests for Android-specific behavior.

Test Coverage

While test coverage is not currently configured, you can add JaCoCo for coverage reports:
1

Add JaCoCo plugin

In app/build.gradle.kts:1:
plugins {
    id("jacoco")
}

android {
    buildTypes {
        debug {
            enableUnitTestCoverage = true
            enableAndroidTestCoverage = true
        }
    }
}
2

Generate coverage report

./gradlew testDebugUnitTest jacocoTestReport
View report at:
app/build/reports/jacoco/jacocoTestReport/html/index.html

Continuous Integration with Vercel

Divvy uses Vercel for CI/CD as mentioned in the README. While primarily for LLM capabilities (transaction categorization), Vercel can also run tests.
The team is exploring Vercel integration for automated testing on every pull request. Stay tuned for updates in Linear or Discord.
For GitHub Actions or similar CI:
name: Android CI

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '11'
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      - name: Run unit tests
        run: ./gradlew test
      - name: Upload test results
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: app/build/reports/tests/

Testing Best Practices

Write Testable Code

  • Inject dependencies: Use Hilt for constructor injection
  • Avoid static state: Use repository patterns, not singletons with mutable state
  • Pure functions: Separate business logic from UI and data layers

Test Naming Conventions

Use descriptive test names:
// Good
@Test
fun `listGroups returns success when data is available`()

// Avoid
@Test
fun test1()

AAA Pattern

Structure tests with Arrange, Act, Assert:
@Test
fun exampleTest() {
    // Arrange - Set up test data and mocks
    val input = "test"

    // Act - Execute the code under test
    val result = functionUnderTest(input)

    // Assert - Verify the outcome
    assertEquals(expected, result)
}

Mock External Dependencies

Use Mockito to mock Supabase and other external services:
@Mock
private lateinit var supabaseClient: SupabaseClient

whenever(supabaseClient.from("groups").select())
    .thenReturn(mockResponse)

Test Edge Cases

  • Empty lists
  • Null values
  • Network errors
  • Invalid user input

Avoid Testing Implementation Details

Focus on behavior, not internal state:
// Good - Tests observable behavior
@Test
fun `button click updates counter`() {
    composeTestRule.onNodeWithTag("counter").assertTextEquals("0")
    composeTestRule.onNodeWithTag("button").performClick()
    composeTestRule.onNodeWithTag("counter").assertTextEquals("1")
}

// Avoid - Tests internal implementation
@Test
fun `viewModel counter variable increments`() {
    assertEquals(0, viewModel.counter) // Accessing private state
}
If tests are brittle and break with every refactor, they’re likely testing implementation details rather than behavior.

Getting Started with Testing

If you’re adding the first tests to Divvy:
1

Start with critical paths

Focus on high-value areas:
  • Authentication flow
  • Expense creation and splitting
  • Group balance calculations
2

Add tests incrementally

Don’t try to reach 100% coverage immediately. Add tests:
  • When fixing bugs (reproduce the bug in a test first)
  • When adding new features (test-driven development)
  • When refactoring (ensure behavior doesn’t change)
3

Document test patterns

Create example test files that others can reference for consistency.

Next Steps