[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](/images/blog/dalendar_dev_6_testing.png)
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!
![[Dalendar DevLog 2] Solid Foundation - Architecture and Data Structure Design](/images/blog/dalendar_dev_2_architecture.png)
![[Dalendar DevLog 3] Database Integration and Schedule Management](/images/blog/dalendar_dev_3_database.png)
![[Dalendar DevLog 7] Advanced Features - Widgets and Notifications](/images/blog/dalendar_dev_7_widgets.png)