Realays Logo Realays
← Back to Blog
DevLog 12/10/2025

[Dalendar DevLog 6] Testing and Quality Assurance

Comprehensive testing strategies including unit tests, integration tests, and UI tests for calendar functionality.

[Dalendar DevLog 6] Testing and Quality Assurance

Episode 6: Testing and Quality Assurance

Introduction

Great software isn’t just about features—it’s about reliability. This episode covers our comprehensive testing strategy, from unit tests for core algorithms to end-to-end UI tests.

1. Testing Philosophy

We follow the testing pyramid:

  • 70% Unit Tests: Fast, isolated, test core logic
  • 20% Integration Tests: Test component interactions
  • 10% UI Tests: Test user workflows

2. Unit Testing Core Algorithms

2.1. EAF Algorithm Tests

class EAFCalculatorTest {
    @Test
    fun `rataDieToYMD converts correctly for known dates`() {
        // Test leap year
        val (y1, m1, d1) = EAFCalculator.rataDieToYMD(738000)
        assertEquals(2020, y1)
        assertEquals(2, m1)  // February
        assertEquals(29, d1)

        // Test year boundary
        val (y2, m2, d2) = EAFCalculator.rataDieToYMD(737425)
        assertEquals(2019, y2)
        assertEquals(1, m2)
        assertEquals(1, d2)
    }

    @Test
    fun `ymdToRataDie is inverse of rataDieToYMD`() {
        val testDates = listOf(
            Triple(2020, 1, 1),
            Triple(2024, 2, 29),  // Leap year
            Triple(2100, 3, 15),  // Non-leap century
            Triple(2000, 12, 31)  // Leap century
        )

        testDates.forEach { (y, m, d) ->
            val rataDie = EAFCalculator.ymdToRataDie(y, m, d)
            val (y2, m2, d2) = EAFCalculator.rataDieToYMD(rataDie)
            assertEquals(y, y2)
            assertEquals(m, m2)
            assertEquals(d, d2)
        }
    }

    @Test
    fun `getDayOfWeek returns correct values`() {
        // 2024-01-01 is Monday (1)
        assertEquals(1, EAFCalculator.getDayOfWeek(2024, 1, 1))

        // 2024-12-25 is Wednesday (3)
        assertEquals(3, EAFCalculator.getDayOfWeek(2024, 12, 25))
    }

    @Test
    fun `performance benchmark meets requirements`() {
        val iterations = 100_000
        val startTime = System.nanoTime()

        repeat(iterations) {
            EAFCalculator.ymdToRataDie(2024, 6, 15)
        }

        val duration = (System.nanoTime() - startTime) / 1_000_000  // ms
        assertTrue(duration < 100, "Too slow: ${duration}ms for $iterations iterations")
    }
}

2.2. Recurrence Rule Tests

class RecurrenceRuleTest {
    @Test
    fun `daily recurrence generates correct occurrences`() {
        val rule = RecurrenceRule.parse("FREQ=DAILY;INTERVAL=1")
        val start = EAFCalculator.ymdToRataDie(2024, 1, 1)
        val occurrences = rule.generateOccurrences(start, count = 5)

        assertEquals(5, occurrences.size)
        assertEquals(start, occurrences[0])
        assertEquals(start + 1, occurrences[1])
        assertEquals(start + 4, occurrences[4])
    }

    @Test
    fun `weekly recurrence on specific days works`() {
        val rule = RecurrenceRule.parse("FREQ=WEEKLY;BYDAY=MO,WE,FR")
        val start = EAFCalculator.ymdToRataDie(2024, 1, 1)  // Monday
        val occurrences = rule.generateOccurrences(start, count = 6)

        // Should be Mon, Wed, Fri, Mon, Wed, Fri
        val days = occurrences.map { EAFCalculator.getDayOfWeek(it) }
        assertEquals(listOf(1, 3, 5, 1, 3, 5), days)
    }
}

3. Repository and Database Tests

3.1. Room DAO Tests

@RunWith(AndroidJUnit4::class)
class ScheduleDaoTest {
    private lateinit var database: DalendarDatabase
    private lateinit var dao: ScheduleDao

    @Before
    fun setup() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        database = Room.inMemoryDatabaseBuilder(context, DalendarDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        dao = database.scheduleDao()
    }

    @After
    fun teardown() {
        database.close()
    }

    @Test
    fun insertAndRetrieveSchedule() = runBlocking {
        val schedule = Schedule(
            title = "Meeting",
            startRataDie = 738000,
            endRataDie = 738000
        )

        dao.insert(schedule)
        val schedules = dao.getSchedulesForDay(738000)

        assertEquals(1, schedules.size)
        assertEquals("Meeting", schedules[0].title)
    }

    @Test
    fun rangeQueryReturnsCorrectSchedules() = runBlocking {
        // Insert schedules across multiple days
        dao.insert(Schedule(title = "Day 1", startRataDie = 738000, endRataDie = 738000))
        dao.insert(Schedule(title = "Day2", startRataDie = 738001, endRataDie = 738001))
        dao.insert(Schedule(title = "Day 5", startRataDie = 738004, endRataDie = 738004))

        // Query range 738000-738002
        val schedules = dao.getSchedulesInRange(738000, 738002)

        assertEquals(2, schedules.size)
        assertTrue(schedules.any { it.title == "Day 1" })
        assertTrue(schedules.any { it.title == "Day 2" })
        assertFalse(schedules.any { it.title == "Day 5" })
    }

    @Test
    fun deleteScheduleWorks() = runBlocking {
        val schedule = Schedule(title = "Test", startRataDie = 738000, endRataDie = 738000)
        val id = dao.insert(schedule)

        dao.deleteById(id)
        val schedules = dao.getSchedulesForDay(738000)

        assertTrue(schedules.isEmpty())
    }
}

3.2. Repository Tests

class ScheduleRepositoryTest {
    private lateinit var dao: ScheduleDao
    private lateinit var repository: ScheduleRepository

    @Before
    fun setup() {
        dao = mock()
        repository = ScheduleRepository(dao)
    }

    @Test
    fun `getSchedulesForMonth queries correct range`() = runBlocking {
        repository.getSchedulesForMonth(2024, 2)  // February 2024

        // Verify correct Rata Die range for February 2024
        verify(dao).getSchedulesInRange(
            eq(EAFCalculator.ymdToRataDie(2024, 2, 1)),
            eq(EAFCalculator.ymdToRataDie(2024, 2, 29))  // Leap year
        )
    }
}

4. ViewModel Tests

4.1. CalendarViewModel Tests

class CalendarViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private lateinit var repository: ScheduleRepository
    private lateinit var viewModel: CalendarViewModel

    @Before
    fun setup() {
        repository = mock()
        viewModel = CalendarViewModel(repository)
    }

    @Test
    fun `loadSchedulesForDay updates state`() = runBlocking {
        val testSchedules = listOf(
            Schedule(title = "Meeting", startRataDie = 738000, endRataDie = 738000)
        )

        whenever(repository.getSchedulesForDay(738000)).thenReturn(testSchedules)

        viewModel.loadSchedulesForDay(738000)

        val state = viewModel.schedulesForDay.getOrAwaitValue()
        assertEquals(testSchedules, state)
    }

    @Test
    fun `addSchedule triggers repository insert`() = runBlocking {
        val schedule = Schedule(title = "New", startRataDie = 738000, endRataDie = 738000)

        viewModel.addSchedule(schedule)

        verify(repository).addSchedule(schedule)
    }
}

5. UI Testing

5.1. Compose UI Tests

@RunWith(AndroidJUnit4::class)
class CalendarScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun calendarDisplaysCurrentMonth() {
        composeTestRule.setContent {
            CalendarScreen(viewModel = CalendarViewModel(mock()))
        }

        // Verify current month header is displayed
        val currentMonth = LocalDate.now().month.name
        composeTestRule.onNodeWithText(currentMonth).assertIsDisplayed()
    }

    @Test
    fun clickingDayShowsSchedules() {
        composeTestRule.setContent {
            CalendarScreen(viewModel = CalendarViewModel(mock()))
        }

        // Click on a day
        composeTestRule.onNodeWithText("15").performClick()

        // Verify day detail sheet appears
        composeTestRule.onNodeWithTag("dayDetailSheet").assertIsDisplayed()
    }

    @Test
    fun addScheduleFABOpensDialog() {
        composeTestRule.setContent {
            CalendarScreen(viewModel = CalendarViewModel(mock()))
        }

        // Click FAB
        composeTestRule.onNodeWithContentDescription("Add Schedule").performClick()

        // Verify dialog appears
        composeTestRule.onNodeWithTag("scheduleDialog").assertIsDisplayed()
    }
}

5.2. Espresso Tests (for traditional Views)

@RunWith(AndroidJUnit4::class)
class MainActivity Test {
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun scrollCalendarShowsDifferentMonths() {
        // Scroll left to previous month
        onView(withId(R.id.monthRecyclerView))
            .perform(swipeLeft())

        // Verify month changed
        // (Would need to check month header text)
    }

    @Test
    fun longPressDateOpensScheduleDialog() {
        onView(withText("15"))
            .perform(longClick())

        onView(withId(R.id.scheduleDialog))
            .check(matches(isDisplayed()))
    }
}

6. Integration Tests

6.1. End-to-End Workflow Tests

@Test
fun completeScheduleCreationWorkflow() = runBlocking {
    // Create schedule
    val schedule = Schedule(
        title = "Doctor Appointment",
        startRataDie = EAFCalculator.ymdToRataDie(2024, 6, 15),
        endRataDie = EAFCalculator.ymdToRataDie(2024, 6, 15),
        startTime = "14:30"
    )

    val id = repository.addSchedule(schedule)

    // Verify it was saved
    val retrieved = repository.getSchedulesForDay(schedule.startRataDie)
    assertEquals(1, retrieved.size)
    assertEquals("Doctor Appointment", retrieved[0].title)

    // Update schedule
    val updated = retrieved[0].copy(startTime = "15:00")
    repository.updateSchedule(updated)

    // Verify update
    val afterUpdate = repository.getSchedulesForDay(schedule.startRataDie)
    assertEquals("15:00", afterUpdate[0].startTime)

    // Delete schedule
    repository.deleteSchedule(afterUpdate[0])

    // Verify deletion
    val afterDelete = repository.getSchedulesForDay(schedule.startRataDie)
    assertTrue(afterDelete.isEmpty())
}

7. Performance Testing

7.1. Load Testing

@Test
fun handleLargeNumberOfSchedules() = runBlocking {
    // Insert 10,000 schedules
    val schedules = (1..10_000).map {
        Schedule(
            title = "Schedule $it",
            startRataDie = 738000 + (it % 365),
            endRataDie = 738000 + (it % 365)
        )
    }

    val startTime = System.currentTimeMillis()
    repository.insertSchedulesBatch(schedules)
    val insertTime = System.currentTimeMillis() - startTime

    assertTrue(insertTime < 5000, "Insert too slow: ${insertTime}ms")

    // Query performance
    val queryStart = System.currentTimeMillis()
    val retrieved = repository.getSchedulesForMonth(2024, 6)
    val queryTime = System.currentTimeMillis() - queryStart

    assertTrue(queryTime < 100, "Query too slow: ${queryTime}ms")
}

8. Edge Case Testing

8.1. Boundary Conditions

@Test
fun handleLeapYearEdgeCases() {
    // Leap year
    val feb292024 = EAFCalculator.ymdToRataDie(2024, 2, 29)
    val (y, m, d) = EAFCalculator.rataDieToYMD(feb292024)
    assertEquals(2024, y)
    assertEquals(2, m)
    assertEquals(29, d)

    // Non-leap year (should handle gracefully)
    assertThrows<IllegalArgumentException> {
        EAFCalculator.ymdToRataDie(2023, 2, 29)
    }

    // Leap century (2000)
    val feb292000 = EAFCalculator.ymdToRataDie(2000, 2, 29)
    assertNotNull(feb292000)

    // Non-leap century (2100)
    assertThrows<IllegalArgumentException> {
        EAFCalculator.ymdToRataDie(2100, 2, 29)
    }
}

@Test
fun handleInvalidDates() {
    assertThrows<IllegalArgumentException> {
        EAFCalculator.ymdToRataDie(2024, 13, 1)  // Invalid month
    }

    assertThrows<IllegalArgumentException> {
        EAFCalculator.ymdToRataDie(2024, 4, 31)  //April has 30 days
    }
}

9. Test Coverage

We maintain >80% test coverage:

./gradlew testDebugUnitTestCoverage

# Results:
# Core algorithms: 95% coverage
# Repository layer: 88% coverage
# ViewModels: 82% coverage
# UI components: 65% coverage
# Overall: 83% coverage

10. Continuous Integration

10.1. GitHub Actions Workflow

name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up JDK
        uses: actions/setup-java@v2
        with:
          java-version: "17"

      - name: Run unit tests
        run: ./gradlew test

      - name: Run instrumented tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 29
          script: ./gradlew connectedAndroidTest

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v2
        with:
          name: test-reports
          path: app/build/reports/tests/

Conclusion

Comprehensive testing ensures:

✅ Core algorithm correctness ✅ Database operations reliability ✅ UI functionality ✅ Performance requirements met ✅ Edge cases handled ✅ Continuous quality monitoring

Testing isn’t overhead—it’s insurance for quality software.

Next Episode Preview:

  • App distribution and release
  • Play Store optimization
  • User feedback collection
  • Future roadmap

Quality first, features second!

Related Posts