From e67c0276f6a05b4434ce717c27b8db6596cf623e Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Sun, 29 Mar 2026 17:32:28 +0200 Subject: [PATCH 1/5] update Java toolchain to version 17 and add unit and instrumentation testing dependencies --- app/build.gradle.kts | 35 +++++++++++++++++++++++------------ gradle/libs.versions.toml | 19 +++++++++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6b1d910..5d33560 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,11 +30,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } buildFeatures { compose = true @@ -50,13 +50,10 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) - debugImplementation(libs.androidx.compose.ui.test.manifest) // Hilt implementation(libs.hilt.android) ksp(libs.hilt.compiler) @@ -73,12 +70,26 @@ dependencies { // Navigation implementation(libs.navigation.compose) - // Tests + // Local Unit Tests + testImplementation(libs.androidx.test.core) testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - debugImplementation(libs.androidx.compose.ui.tooling) + testImplementation(libs.androidx.arch.core.testing) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.google.truth) + testImplementation(libs.okhttp.mockwebserver) + testImplementation(libs.mockk) debugImplementation(libs.androidx.compose.ui.test.manifest) + + // Instrumentation tests + androidTestImplementation(libs.hilt.android.testing) + kspAndroidTest(libs.hilt.compiler) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.androidx.arch.core.testing) + androidTestImplementation(libs.google.truth) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.test.core.ktx) + androidTestImplementation(libs.okhttp.mockwebserver) + androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.androidx.test.runner) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a8c6be0..df609c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,11 @@ ksp = "2.0.21-1.0.25" room = "2.6.1" coroutines = "1.9.0" navigation = "2.8.5" +androidxTestCore = "1.6.1" +archCoreTesting = "2.2.0" +truth = "1.4.4" +mockwebserver = "4.12.0" +mockk = "1.13.13" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -45,6 +50,20 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx- # Navigation navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +# Unit Testing +androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" } +androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "archCoreTesting" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } +google-truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } +okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "mockwebserver" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } + +# Instrumentation Testing +hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } +androidx-test-runner = { group = "androidx.test", name = "runner", version = "1.6.2" } +mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } +androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidxTestCore" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From a149b8338fc33ef6383a903d59526e57c799a7da Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Sun, 29 Mar 2026 18:45:52 +0200 Subject: [PATCH 2/5] add FakeTestNoteRepository --- .../data/repository/FakeNoteRepository.kt | 37 +++++++------------ .../java/com/bober/notesapp/di/AppModule.kt | 7 +--- app/src/main/res/values/strings.xml | 4 +- .../com/bober/notesapp/ExampleUnitTest.kt | 17 --------- .../data/repository/FakeTestNoteRepository.kt | 28 ++++++++++++++ 5 files changed, 44 insertions(+), 49 deletions(-) delete mode 100644 app/src/test/java/com/bober/notesapp/ExampleUnitTest.kt create mode 100644 app/src/test/java/com/bober/notesapp/data/repository/FakeTestNoteRepository.kt diff --git a/app/src/main/java/com/bober/notesapp/data/repository/FakeNoteRepository.kt b/app/src/main/java/com/bober/notesapp/data/repository/FakeNoteRepository.kt index 4acf62d..667c8c5 100644 --- a/app/src/main/java/com/bober/notesapp/data/repository/FakeNoteRepository.kt +++ b/app/src/main/java/com/bober/notesapp/data/repository/FakeNoteRepository.kt @@ -1,52 +1,43 @@ package com.bober.notesapp.data.repository -import android.app.Application -import com.bober.notesapp.R import com.bober.notesapp.domain.model.Note import com.bober.notesapp.domain.repository.NoteRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import javax.inject.Inject -class FakeNoteRepository @Inject constructor( - private val app: Application -): NoteRepository { +class FakeNoteRepository @Inject constructor(): NoteRepository { + + private val fakeContent = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vestibulum vel arcu vel ullamcorper. Integer venenatis diam ac facilisis accumsan. Phasellus lorem felis, sollicitudin non justo ut, posuere dignissim nunc. Nunc dignissim volutpat mauris quis rhoncus. Curabitur tempor ipsum non lacus porttitor mollis. Etiam dictum eros eros, at auctor augue consequat a. Fusce a arcu ac est maximus hendrerit at lacinia tellus.In feugiat dui non laoreet vestibulum. Nunc vulputate id mauris a iaculis. Curabitur vitae tempus justo. Morbi a est et sapien maximus porta eu ac ante. Cras gravida feugiat elementum. Vestibulum dictum volutpat est sit amet commodo. Suspendisse sed lectus magna. Pellentesque eget augue sed ipsum aliquet tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla sed auctor arcu. Proin ligula nisl, auctor ac ornare eu, mattis quis lorem. Phasellus et gravida metus, vitae tincidunt nibh. Nam tempus, libero porttitor mattis suscipit, neque neque viverra nunc, non ultrices nibh odio non turpis. Vestibulum dui sem, congue vel tellus ac, convallis iaculis mauris. Integer vulputate, augue bibendum congue ullamcorper, lacus enim luctus magna, sed iaculis magna enim eget augue." // This replaces a real database during development or testing - private val _notesFlow = MutableStateFlow>( + private val notesFlow = MutableStateFlow( listOf( - Note(id = 1, title = "First Note", content = app.getString(R.string.fake_content), timestamp = 1L, color = 0xFFFFAB91.toInt()), - Note(id = 2, title = "Second Note", content = app.getString(R.string.fake_content), timestamp = 2L, color = 0xFFE7ED9B.toInt()), - Note(id = 3, title = "Third Note", content = app.getString(R.string.fake_content), timestamp = 3L, color = 0xFFD7AEFB.toInt()) + Note(id = 1, title = "First Note", content = fakeContent, timestamp = 1L, color = 0xFFFFAB91.toInt()), + Note(id = 2, title = "Second Note", content = fakeContent, timestamp = 2L, color = 0xFFE7ED9B.toInt()), + Note(id = 3, title = "Third Note", content = fakeContent, timestamp = 3L, color = 0xFFD7AEFB.toInt()) ) ) + override fun getNotes(): Flow> { - return _notesFlow.asStateFlow() + return notesFlow } override suspend fun getNoteById(id: Int): Note? { - return _notesFlow.value.find { it.id == id } + return notesFlow.value.find { it.id == id } } override suspend fun insertNote(note: Note) { - _notesFlow.update { currentNotes -> - val existingNote = currentNotes.find { it.id == note.id } - if (existingNote != null) { - currentNotes.map { if (it.id == note.id) note else it } - } else { - val newId = (currentNotes.maxOfOrNull { it.id ?: 0 } ?: 0) + 1 - currentNotes + note.copy(id = newId) - } + notesFlow.update { currentNotes -> + currentNotes + note } } override suspend fun deleteNote(note: Note) { - println("Repository: Deleting note ${note.id}") - _notesFlow.update { currentNotes -> + notesFlow.update { currentNotes -> currentNotes.filter { it.id != note.id } } - println("Repository: Remaining notes: ${_notesFlow.value.size}") } } \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/di/AppModule.kt b/app/src/main/java/com/bober/notesapp/di/AppModule.kt index 3b09b26..130c236 100644 --- a/app/src/main/java/com/bober/notesapp/di/AppModule.kt +++ b/app/src/main/java/com/bober/notesapp/di/AppModule.kt @@ -30,11 +30,6 @@ object AppModule { @Singleton fun provideNoteRepository(db : NoteDatabase): NoteRepository{ return NoteRepositoryImpl(db.noteDao()) + // return FakeNoteRepository() } - -// @Provides -// @Singleton -// fun provideNoteRepository(app: Application): NoteRepository{ -// return FakeNoteRepository(app) -// } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d8ff1a..1943441 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,4 @@ NotesApp - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vestibulum vel arcu vel ullamcorper. Integer venenatis diam ac facilisis accumsan. Phasellus lorem felis, sollicitudin non justo ut, posuere dignissim nunc. Nunc dignissim volutpat mauris quis rhoncus. Curabitur tempor ipsum non lacus porttitor mollis. Etiam dictum eros eros, at auctor augue consequat a. Fusce a arcu ac est maximus hendrerit at lacinia tellus. - -In feugiat dui non laoreet vestibulum. Nunc vulputate id mauris a iaculis. Curabitur vitae tempus justo. Morbi a est et sapien maximus porta eu ac ante. Cras gravida feugiat elementum. Vestibulum dictum volutpat est sit amet commodo. Suspendisse sed lectus magna. Pellentesque eget augue sed ipsum aliquet tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla sed auctor arcu. Proin ligula nisl, auctor ac ornare eu, mattis quis lorem. Phasellus et gravida metus, vitae tincidunt nibh. Nam tempus, libero porttitor mattis suscipit, neque neque viverra nunc, non ultrices nibh odio non turpis. Vestibulum dui sem, congue vel tellus ac, convallis iaculis mauris. Integer vulputate, augue bibendum congue ullamcorper, lacus enim luctus magna, sed iaculis magna enim eget augue. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vestibulum vel arcu vel ullamcorper. Integer venenatis diam ac facilisis accumsan. Phasellus lorem felis, sollicitudin non justo ut, posuere dignissim nunc. Nunc dignissim volutpat mauris quis rhoncus. Curabitur tempor ipsum non lacus porttitor mollis. Etiam dictum eros eros, at auctor augue consequat a. Fusce a arcu ac est maximus hendrerit at lacinia tellus.In feugiat dui non laoreet vestibulum. Nunc vulputate id mauris a iaculis. Curabitur vitae tempus justo. Morbi a est et sapien maximus porta eu ac ante. Cras gravida feugiat elementum. Vestibulum dictum volutpat est sit amet commodo. Suspendisse sed lectus magna. Pellentesque eget augue sed ipsum aliquet tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla sed auctor arcu. Proin ligula nisl, auctor ac ornare eu, mattis quis lorem. Phasellus et gravida metus, vitae tincidunt nibh. Nam tempus, libero porttitor mattis suscipit, neque neque viverra nunc, non ultrices nibh odio non turpis. Vestibulum dui sem, congue vel tellus ac, convallis iaculis mauris. Integer vulputate, augue bibendum congue ullamcorper, lacus enim luctus magna, sed iaculis magna enim eget augue. \ No newline at end of file diff --git a/app/src/test/java/com/bober/notesapp/ExampleUnitTest.kt b/app/src/test/java/com/bober/notesapp/ExampleUnitTest.kt deleted file mode 100644 index de906ef..0000000 --- a/app/src/test/java/com/bober/notesapp/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.bober.notesapp - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/app/src/test/java/com/bober/notesapp/data/repository/FakeTestNoteRepository.kt b/app/src/test/java/com/bober/notesapp/data/repository/FakeTestNoteRepository.kt new file mode 100644 index 0000000..92af543 --- /dev/null +++ b/app/src/test/java/com/bober/notesapp/data/repository/FakeTestNoteRepository.kt @@ -0,0 +1,28 @@ +package com.bober.notesapp.data.repository + +import com.bober.notesapp.domain.model.Note +import com.bober.notesapp.domain.repository.NoteRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class FakeTestNoteRepository @Inject constructor(): NoteRepository { + + private val notes = mutableListOf() + + override fun getNotes(): Flow> { + return flow { emit(notes) } + } + + override suspend fun getNoteById(id: Int): Note? { + return notes.find { it.id == id } + } + + override suspend fun insertNote(note: Note) { + notes.add(note) + } + + override suspend fun deleteNote(note: Note) { + notes.remove(note) + } +} \ No newline at end of file From 71ca3b144a1f01de3f85678068199e68a6649494 Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Sun, 29 Mar 2026 18:46:04 +0200 Subject: [PATCH 3/5] add GetNotesTest for testing note sorting logic --- .../notesapp/domain/use_case/GetNotesTest.kt | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 app/src/test/java/com/bober/notesapp/domain/use_case/GetNotesTest.kt diff --git a/app/src/test/java/com/bober/notesapp/domain/use_case/GetNotesTest.kt b/app/src/test/java/com/bober/notesapp/domain/use_case/GetNotesTest.kt new file mode 100644 index 0000000..6b18948 --- /dev/null +++ b/app/src/test/java/com/bober/notesapp/domain/use_case/GetNotesTest.kt @@ -0,0 +1,93 @@ +package com.bober.notesapp.domain.use_case + +import com.bober.notesapp.data.repository.FakeTestNoteRepository +import com.bober.notesapp.domain.model.Note +import com.bober.notesapp.domain.util.NoteOrder +import com.bober.notesapp.domain.util.OrderType +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class GetNotesTest { + + private lateinit var getNotes: GetNotes + private lateinit var fakeRepository: FakeTestNoteRepository + + @Before + fun setUp(){ + fakeRepository = FakeTestNoteRepository() + getNotes = GetNotes(fakeRepository) + + val notesToInsert = mutableListOf() + ('a'..'z').forEachIndexed { index, ch -> + notesToInsert.add( + Note( + title = ch.toString(), + content = ch.toString(), + timestamp = index.toLong(), + color = index + ) + ) + } + notesToInsert.shuffle() + runTest { + notesToInsert.forEach { fakeRepository.insertNote(it) } + } + } + + @Test + fun `Order notes by title ascending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Title(OrderType.Ascending)).first() + + for (i in 0..notes.size-2){ + assertThat(notes[i].title).isLessThan(notes[i+1].title) + } + } + + @Test + fun `Order notes by title descending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Title(OrderType.Descending)).first() + + for (i in 0..notes.size - 2) { + assertThat(notes[i].title).isGreaterThan(notes[i + 1].title) + } + } + + @Test + fun `Order notes by date ascending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Date(OrderType.Ascending)).first() + + for (i in 0..notes.size - 2) { + assertThat(notes[i].timestamp).isLessThan(notes[i + 1].timestamp) + } + } + + @Test + fun `Order notes by date descending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Date(OrderType.Descending)).first() + + for (i in 0..notes.size - 2) { + assertThat(notes[i].timestamp).isGreaterThan(notes[i + 1].timestamp) + } + } + + @Test + fun `Order notes by color ascending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Color(OrderType.Ascending)).first() + + for (i in 0..notes.size - 2) { + assertThat(notes[i].color).isLessThan(notes[i + 1].color) + } + } + + @Test + fun `Order notes by color descending, correct order`() = runTest { + val notes = getNotes(NoteOrder.Color(OrderType.Descending)).first() + + for (i in 0..notes.size - 2) { + assertThat(notes[i].color).isGreaterThan(notes[i + 1].color) + } + } +} \ No newline at end of file From 90e88c0597fe34850337830a09a454f22f9e4d0e Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Sun, 29 Mar 2026 19:08:28 +0200 Subject: [PATCH 4/5] add AddNoteTest for AddNote use case --- .../notesapp/domain/use_case/AddNoteTest.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 app/src/test/java/com/bober/notesapp/domain/use_case/AddNoteTest.kt diff --git a/app/src/test/java/com/bober/notesapp/domain/use_case/AddNoteTest.kt b/app/src/test/java/com/bober/notesapp/domain/use_case/AddNoteTest.kt new file mode 100644 index 0000000..b23fbb1 --- /dev/null +++ b/app/src/test/java/com/bober/notesapp/domain/use_case/AddNoteTest.kt @@ -0,0 +1,73 @@ +package com.bober.notesapp.domain.use_case + +import com.bober.notesapp.data.repository.FakeTestNoteRepository +import com.bober.notesapp.domain.model.InvalidNoteException +import com.bober.notesapp.domain.model.Note +import kotlinx.coroutines.test.runTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class AddNoteTest { + private lateinit var addNote: AddNote + private lateinit var fakeRepository: FakeTestNoteRepository + + @Before + fun setUp() { + fakeRepository = FakeTestNoteRepository() + addNote = AddNote(fakeRepository) + } + + @Test + fun `Insert valid note, successfully adds to repository`() = runTest { + val note = Note( + title = "Not blank", + content = "Not blank", + timestamp = 1L, + color = 1, + id = 99 + ) + addNote(note) + + val result = fakeRepository.getNoteById(99) + assertThat(result).isEqualTo(note) + } + + @Test + fun `Insert note with blank title, throws exception`() = runTest { + val note = Note( + title = "", + content = "Not blank", + timestamp = 1L, + color = 1, + id = 99 + ) + + val exception = try { + addNote(note) + null + } catch (e: InvalidNoteException){ + e + } + assertThat(exception?.message).isEqualTo("The title of the note can't be empty.") + } + + @Test + fun `Insert note with blank content, throws exception`() = runTest { + val note = Note( + title = "Not blank", + content = "", + timestamp = 1L, + color = 1, + id = 99 + ) + + val exception = try { + addNote(note) + null + } catch (e: InvalidNoteException){ + e + } + assertThat(exception?.message).isEqualTo("The content of the note can't be empty.") + } +} \ No newline at end of file From 745aa647fa47675f003b19e989f4ee1c652d39df Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Sun, 29 Mar 2026 19:19:06 +0200 Subject: [PATCH 5/5] add Android CI workflow for building and testing --- .github/workflows/android.yml | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/android.yml diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..1de72da --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,44 @@ +name: Android CI + +# Define when the pipeline should run +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "master", "main" ] + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + # Download the code from the repository + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java 17 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + # Make the Gradle wrapper executable + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Run Local Unit Tests + - name: Run local Unit Tests + run: ./gradlew testDebugUnitTest + + # Build the APK to ensure the project compiles correctly + - name: Build Debug APK + run: ./gradlew assembleDebug + + # Upload the APK as an artifact so you can download it from GitHub + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: app/build/outputs/apk/debug/app-debug.apk \ No newline at end of file