diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..bc2bb57 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,61 @@ +name: Android CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Run lint + run: ./gradlew lint + + - name: Upload lint report + uses: actions/upload-artifact@v4 + with: + name: lint-report + path: presentation/build/reports/lint-results-debug.html + + unit-test: + needs: [lint] + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Run tests + run: ./gradlew test + + - name: Upload test report + uses: actions/upload-artifact@v4 + with: + name: unit-test-report + path: app/build/reports/tests/testDevelopmentDebugUnitTest/ + +# instrumentation-test: +# needs: [unit-test] +# runs-on: macos-latest +# steps: +# - name: Checkout the code +# uses: actions/checkout@v4 +# +# - name: Run espresso tests +# uses: reactivecircus/android-emulator-runner@v2 +# with: +# api-level: 35 +# script: ./gradlew connectedCheck +# startup-timeout: 5m +# +# - name: Upload test report +# uses: actions/upload-artifact@v4 +# with: +# name: instrumentation_test_report +# path: app/build/reports/androidTests/connected/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index add751b..3970d22 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,10 +117,10 @@ dependencies { // Unit Test testImplementation(libs.junit) - testImplementation(libs.mockito) testImplementation(libs.robolectric) testImplementation(libs.androidx.core.testing) testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockito) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockk) testImplementation(libs.test.rules) diff --git a/app/src/androidTest/java/com/mkdev/nimblesurvey/screen/authentication/signin/SignInScreenTest.kt b/app/src/androidTest/java/com/mkdev/nimblesurvey/screen/authentication/signin/SignInScreenTest.kt index 9a2d346..10e7cf1 100644 --- a/app/src/androidTest/java/com/mkdev/nimblesurvey/screen/authentication/signin/SignInScreenTest.kt +++ b/app/src/androidTest/java/com/mkdev/nimblesurvey/screen/authentication/signin/SignInScreenTest.kt @@ -70,7 +70,6 @@ class SignInScreenTest { composeTestRule.onNodeWithText("Password").performTextInput("12345678") composeTestRule.onNodeWithText("Log in").performClick() - // Wait for navigation to complete (adjust timeout as needed) composeTestRule.waitUntil(timeoutMillis = 2000) { navController.currentDestination?.route == HomeNavigation.ROUTE } @@ -94,7 +93,6 @@ class SignInScreenTest { fun signInScreen_forgotPasswordClick_navigatesToForgotPassword() { composeTestRule.onNodeWithText("Forgot?").performClick() - // Wait for navigation to complete (adjust timeout as needed) composeTestRule.waitUntil(timeoutMillis = 2000) { navController.currentDestination?.route == ResetPasswordNavigation.ROUTE } diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09b..4a4b17f 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,7 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09b..4a4b17f 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,7 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..55344e5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,3 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts index d17851f..5abb81e 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -83,12 +83,20 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.mockito.kotlin) testImplementation(libs.mockk) - androidTestImplementation(libs.androidx.espresso.core) testImplementation(libs.test.rules) - androidTestImplementation(libs.room.testing) testImplementation(libs.androidx.junit) testImplementation(libs.turbine) testImplementation(libs.truth) + + // UI Test + androidTestImplementation(libs.room.testing) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.hilt.android.testing) + androidTestImplementation(libs.test.runner) + androidTestImplementation(libs.test.rules) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.turbine) + androidTestImplementation(libs.kotlinx.coroutines.test) } protobuf { diff --git a/data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt b/data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt similarity index 75% rename from data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt rename to data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt index d60174f..595007c 100644 --- a/data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt +++ b/data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyDaoTest.kt @@ -1,6 +1,6 @@ package com.mkdev.data.datasource.local.database.room.dao -import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import android.content.Context import androidx.paging.PagingSource import androidx.room.Room import androidx.test.core.app.ApplicationProvider @@ -8,9 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.mkdev.data.datasource.local.database.room.NimbleRoomDatabase import com.mkdev.data.datasource.local.database.room.entity.SurveyEntity import com.mkdev.data.factory.SurveyEntityFactory -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher +import com.mkdev.data.utils.TestDispatcherRule import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals @@ -19,26 +17,23 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class SurveyDaoTest { - @get:Rule - var instantExecutorRule = InstantTaskExecutorRule() + @get: Rule + val dispatcherRule = TestDispatcherRule() private lateinit var database: NimbleRoomDatabase private lateinit var surveyDao: SurveyDao - private lateinit var testDispatcher:TestDispatcher @Before fun setUp() { + val context = ApplicationProvider.getApplicationContext() database = Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), + context, NimbleRoomDatabase::class.java - ).allowMainThreadQueries().build() + ).build() surveyDao = database.surveyDao() - - testDispatcher = StandardTestDispatcher() } @After @@ -47,7 +42,7 @@ class SurveyDaoTest { } @Test - fun `insertAll should insert surveys into database`() = runTest(testDispatcher) { + fun insertAll_should_insert_surveys_into_database() = runTest { // Given val surveys = SurveyEntityFactory.createSurveyEntityList(count = 2) @@ -63,7 +58,7 @@ class SurveyDaoTest { } @Test - fun `getByPaging should return paging source of surveys`() = runTest(testDispatcher) { + fun getByPaging_should_return_paging_source_of_surveys() = runTest { // Given val surveys = SurveyEntityFactory.createSurveyEntityList(count = 2) surveyDao.insertAll(surveys) @@ -83,7 +78,7 @@ class SurveyDaoTest { } @Test - fun `getById should return survey by id`() = runTest(testDispatcher) { + fun getById_should_return_survey_by_id() = runTest { // Given val survey = SurveyEntityFactory.createSurveyEntity() surveyDao.insertAll(listOf(survey)) @@ -96,7 +91,7 @@ class SurveyDaoTest { } @Test - fun `getById should return null when survey not found`() = runTest(testDispatcher) { + fun getById_should_return_null_when_survey_not_found() = runTest { // Given val nonexistentId = "nonexistent_id" @@ -108,7 +103,7 @@ class SurveyDaoTest { } @Test - fun `clearAll should clear all surveys from database`() = runTest(testDispatcher) { + fun clearAll_should_clear_all_surveys_from_database() = runTest { // Given val surveys = SurveyEntityFactory.createSurveyEntityList(count = 2) surveyDao.insertAll(surveys) diff --git a/data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt b/data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt similarity index 77% rename from data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt rename to data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt index b860296..92cd655 100644 --- a/data/src/test/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt +++ b/data/src/androidTest/java/com/mkdev/data/datasource/local/database/room/dao/SurveyRemoteKeyDaoTest.kt @@ -1,32 +1,39 @@ -package com.mkdev.data.datasource.local.database.room.dao +package com.mkdev.data.datasource.local.database.room.dao//package com.mkdev.data.datasource.local.database.room.dao +import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.mkdev.data.datasource.local.database.room.NimbleRoomDatabase import com.mkdev.data.datasource.local.database.room.entity.SurveyRemoteKeyEntity import com.mkdev.data.factory.SurveyRemoteKeyEntityFactory +import com.mkdev.data.utils.TestDispatcherRule import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SurveyRemoteKeyDaoTest { + @get: Rule + val dispatcherRule = TestDispatcherRule() + private lateinit var database: NimbleRoomDatabase private lateinit var surveyRemoteKeyDao: SurveyRemoteKeyDao private val testDispatcher = StandardTestDispatcher() @Before fun setUp() { + val context = ApplicationProvider.getApplicationContext() database = Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), + context, NimbleRoomDatabase::class.java - ).allowMainThreadQueries().build() + ).build() surveyRemoteKeyDao = database.surveyRemoteKeyDao() } @@ -36,7 +43,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `insertAll should insert remote keys into database`() = runTest(testDispatcher) { + fun insertAll_should_insert_remote_keys_into_database() = runTest { // Given val remoteKeys = SurveyRemoteKeyEntityFactory.createSurveyRemoteKeyEntityList(count = 2) @@ -49,7 +56,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `insert should insert a single remote key into database`() = runTest(testDispatcher) { + fun insert_should_insert_a_single_remote_key_into_database() = runTest(testDispatcher) { // Given val remoteKey = SurveyRemoteKeyEntityFactory.createSurveyRemoteKeyEntity() @@ -62,7 +69,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `insertOrReplace should insert or replace a remote key`() = runTest(testDispatcher) { + fun insertOrReplace_should_insert_or_replace_a_remote_key() = runTest(testDispatcher) { // Given val remoteKey1 = SurveyRemoteKeyEntityFactory.createSurveyRemoteKeyEntity() val remoteKey2 = remoteKey1.copy(nextPage = 3) // Updated nextPage @@ -77,7 +84,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `remoteKeysId should return remote key by id`() = runTest(testDispatcher) { + fun remoteKeysId_should_return_remote_key_by_id() = runTest(testDispatcher) { // Given val remoteKey = SurveyRemoteKeyEntityFactory.createSurveyRemoteKeyEntity() surveyRemoteKeyDao.insert(remoteKey) @@ -90,7 +97,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `remoteKeysId should return null when remote key not found`() = runTest(testDispatcher) { + fun remoteKeysId_should_return_null_when_remote_key_not_found() = runTest(testDispatcher) { // Given val nonexistentId = "nonexistent_id" @@ -102,7 +109,7 @@ class SurveyRemoteKeyDaoTest { } @Test - fun `clearRemoteKeys should clear all remote keys from database`() = runTest(testDispatcher) { + fun clearRemoteKeys_should_clear_all_remote_keys_from_database() = runTest(testDispatcher) { // Given val remoteKeys = SurveyRemoteKeyEntityFactory.createSurveyRemoteKeyEntityList(count = 2) surveyRemoteKeyDao.insertAll(remoteKeys) diff --git a/data/src/androidTest/java/com/mkdev/data/factory/SurveyEntityFactory.kt b/data/src/androidTest/java/com/mkdev/data/factory/SurveyEntityFactory.kt new file mode 100644 index 0000000..5bc0cc8 --- /dev/null +++ b/data/src/androidTest/java/com/mkdev/data/factory/SurveyEntityFactory.kt @@ -0,0 +1,30 @@ +package com.mkdev.data.factory + +import com.mkdev.data.datasource.local.database.room.entity.SurveyEntity + +object SurveyEntityFactory { + + fun createSurveyEntity( + id: String = "survey_id", + title: String = "Survey Title", + description: String = "Survey Description", + coverImageUrl: String = "https://example.com/image.jpg", + isActive: Boolean = true, + surveyType: String = "customer_satisfaction" + ): SurveyEntity { + return SurveyEntity( + id = id, + title = title, + description = description, + coverImageUrl = coverImageUrl, + isActive = isActive, + surveyType = surveyType + ) + } + + fun createSurveyEntityList(count: Int = 5): List { + return (1..count).map { + createSurveyEntity(id = "survey_id_$it") + } + } +} \ No newline at end of file diff --git a/data/src/androidTest/java/com/mkdev/data/factory/SurveyRemoteKeyEntityFactory.kt b/data/src/androidTest/java/com/mkdev/data/factory/SurveyRemoteKeyEntityFactory.kt new file mode 100644 index 0000000..b6b7ae2 --- /dev/null +++ b/data/src/androidTest/java/com/mkdev/data/factory/SurveyRemoteKeyEntityFactory.kt @@ -0,0 +1,23 @@ +package com.mkdev.data.factory + +import com.mkdev.data.datasource.local.database.room.entity.SurveyRemoteKeyEntity + +object SurveyRemoteKeyEntityFactory { + fun createSurveyRemoteKeyEntity( + surveyId: String = "survey_id", + prevPage: Int? = 1, + nextPage: Int? = 2 + ): SurveyRemoteKeyEntity { + return SurveyRemoteKeyEntity( + surveyId = surveyId, + prevPage = prevPage, + nextPage = nextPage + ) + } + + fun createSurveyRemoteKeyEntityList(count: Int = 5): List { + return (1..count).map { + createSurveyRemoteKeyEntity(surveyId = "survey_id_$it") + } + } +} \ No newline at end of file diff --git a/data/src/androidTest/java/com/mkdev/data/utils/TestDispatcherRule.kt b/data/src/androidTest/java/com/mkdev/data/utils/TestDispatcherRule.kt new file mode 100644 index 0000000..d4d26c7 --- /dev/null +++ b/data/src/androidTest/java/com/mkdev/data/utils/TestDispatcherRule.kt @@ -0,0 +1,23 @@ +package com.mkdev.data.utils + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@OptIn(ExperimentalCoroutinesApi::class) +class TestDispatcherRule( + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() +): TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index fca09bf..9dfe08e 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -6,6 +6,10 @@ plugins { java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + + toolchain { + languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.toString())) + } } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69e7dcf..1938739 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ agp = "8.7.3" kotlin = "2.1.0" coreKtx = "1.15.0" junit = "4.13.2" -mockito = "5.12.0" +mockito = "5.14.2" junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.7" @@ -30,7 +30,7 @@ protobufJavalite = "4.29.1" protobuf = "0.9.4" tinkAndroid = "1.7.0" roomRuntime = "2.6.1" -robolectric = "4.11.1" +robolectric = "4.14.1" androidxCoreTesting = "2.2.0" testRules = "1.6.1" kotlinxCoroutinesTest = "1.9.0" diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 046a85c..72cbb55 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -26,6 +26,13 @@ android { compose = true buildConfig = true } + + flavorDimensions += "environment" + productFlavors { + register("development") + register("staging") + register("production") + } } dependencies {