diff --git a/build-logic/convention/src/main/java/SeeDocsFeatureConventionPlugin.kt b/build-logic/convention/src/main/java/SeeDocsFeatureConventionPlugin.kt
index d98a9e1..7fb2b82 100644
--- a/build-logic/convention/src/main/java/SeeDocsFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/java/SeeDocsFeatureConventionPlugin.kt
@@ -1,5 +1,6 @@
 import kr.co.convention.implementations
 import kr.co.convention.libs
+import kr.co.convention.testImplementations
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.kotlin.dsl.dependencies
@@ -21,6 +22,10 @@ class SeeDocsFeatureConventionPlugin : Plugin<Project> {
                     project(":core:model"),
                     libs.koin.compose
                 )
+
+                testImplementations(
+                    project(":core:testing")
+                )
             }
         }
     }
diff --git a/build-logic/convention/src/main/java/SeeDocsLibraryConventionPlugin.kt b/build-logic/convention/src/main/java/SeeDocsLibraryConventionPlugin.kt
index 35eb190..b80be3d 100644
--- a/build-logic/convention/src/main/java/SeeDocsLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/java/SeeDocsLibraryConventionPlugin.kt
@@ -37,10 +37,10 @@ class SeeDocsLibraryConventionPlugin : Plugin<Project> {
                     libs.kotlinx.coroutines.android
                 )
                 testImplementations(
-                    kotlin("test")
+                    kotlin("test"),
                 )
                 androidTestImplementations(
-                    kotlin("test")
+                    kotlin("test"),
                 )
             }
         }
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index f7f2090..2b0e81a 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -9,7 +9,7 @@ android {
 }
 
 dependencies {
+    api(projects.core.model)
     implementation(projects.core.common)
-    implementation(projects.core.model)
     implementation(projects.core.database)
 }
\ No newline at end of file
diff --git a/core/testing/.gitignore b/core/testing/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/core/testing/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts
new file mode 100644
index 0000000..8d27bc6
--- /dev/null
+++ b/core/testing/build.gradle.kts
@@ -0,0 +1,17 @@
+import kr.co.convention.setNamespace
+
+plugins {
+    alias(libs.plugins.seedocs.library)
+}
+
+setNamespace("core.testing")
+
+dependencies {
+    api(libs.mockk)
+    api(libs.kotlinx.coroutines.test)
+    api(libs.mockk.android)
+    api(libs.koin.test)
+    api(libs.koin.junit)
+    api(projects.core.data)
+    api(libs.turbine)
+}
\ No newline at end of file
diff --git a/core/testing/src/main/java/kr/co/testing/repository/TestRecentRepository.kt b/core/testing/src/main/java/kr/co/testing/repository/TestRecentRepository.kt
new file mode 100644
index 0000000..72f04c0
--- /dev/null
+++ b/core/testing/src/main/java/kr/co/testing/repository/TestRecentRepository.kt
@@ -0,0 +1,24 @@
+package kr.co.testing.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kr.co.data.repository.RecentRepository
+import kr.co.model.FileInfo
+
+class TestRecentRepository: RecentRepository {
+
+    private val recentFilesFlow: MutableStateFlow<List<FileInfo>> =
+        MutableStateFlow(emptyList())
+
+    override suspend fun insert(recentFile: FileInfo) {
+        recentFilesFlow.update { it + recentFile }
+    }
+
+    override fun get(): Flow<List<FileInfo>> =
+        recentFilesFlow
+
+    override suspend fun delete(recentFile: FileInfo) {
+        recentFilesFlow.update { it - recentFile }
+    }
+}
\ No newline at end of file
diff --git a/core/testing/src/main/java/kr/co/testing/rule/CoroutineTestRule.kt b/core/testing/src/main/java/kr/co/testing/rule/CoroutineTestRule.kt
new file mode 100644
index 0000000..c995564
--- /dev/null
+++ b/core/testing/src/main/java/kr/co/testing/rule/CoroutineTestRule.kt
@@ -0,0 +1,19 @@
+package kr.co.testing.rule
+
+import kotlinx.coroutines.Dispatchers
+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
+
+class CoroutineTestRule : TestWatcher() {
+    override fun starting(description: Description?) {
+        super.starting(description)
+        Dispatchers.setMain(UnconfinedTestDispatcher())
+    }
+
+    override fun finished(description: Description?) {
+        Dispatchers.resetMain()
+    }
+}
\ No newline at end of file
diff --git a/feature/explore/build.gradle.kts b/feature/explore/build.gradle.kts
index 8befd5f..02c4b72 100644
--- a/feature/explore/build.gradle.kts
+++ b/feature/explore/build.gradle.kts
@@ -5,4 +5,7 @@ plugins {
     alias(libs.plugins.seedocs.library.compose)
 }
 
-setNamespace("feature.explore")
\ No newline at end of file
+setNamespace("feature.explore")
+dependencies {
+    implementation(libs.firebase.crashlytics.buildtools)
+}
diff --git a/feature/explore/src/main/java/kr/co/di/ExploreModule.kt b/feature/explore/src/main/java/kr/co/di/ExploreModule.kt
index 6327709..6254928 100644
--- a/feature/explore/src/main/java/kr/co/di/ExploreModule.kt
+++ b/feature/explore/src/main/java/kr/co/di/ExploreModule.kt
@@ -1,6 +1,8 @@
 package kr.co.di
 
 import kr.co.explore.ExploreViewModel
+import kr.co.util.FileManagerImpl
+import kr.co.util.FileManager
 import org.koin.core.module.dsl.viewModel
 import org.koin.dsl.module
 
@@ -8,7 +10,12 @@ val exploreModule =
     module {
         viewModel {
             ExploreViewModel(
-                get()
+                get(),
+                get<FileManager>(),
             )
         }
+
+        single<FileManager> {
+            FileManagerImpl()
+        }
     }
diff --git a/feature/explore/src/main/java/kr/co/explore/ExploreScreen.kt b/feature/explore/src/main/java/kr/co/explore/ExploreScreen.kt
index f659708..ee8f5a4 100644
--- a/feature/explore/src/main/java/kr/co/explore/ExploreScreen.kt
+++ b/feature/explore/src/main/java/kr/co/explore/ExploreScreen.kt
@@ -25,14 +25,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import kr.co.model.ExploreSideEffect
 import kr.co.model.ExploreUiIntent
 import kr.co.model.ExploreUiState
-import kr.co.model.FileInfo
 import kr.co.seedocs.feature.explore.R
 import kr.co.ui.theme.SeeDocsTheme
 import kr.co.ui.theme.Theme
 import kr.co.ui.util.LaunchIntentHandler
 import kr.co.ui.util.LaunchSideEffect
 import kr.co.ui.widget.FileBox
-import kr.co.util.DEFAULT_STORAGE
+import kr.co.util.FileManagerImpl.Companion.DEFAULT_STORAGE
 import kr.co.widget.FolderBox
 import org.koin.androidx.compose.koinViewModel
 
diff --git a/feature/explore/src/main/java/kr/co/explore/ExploreViewModel.kt b/feature/explore/src/main/java/kr/co/explore/ExploreViewModel.kt
index 7e63f70..bd1cac0 100644
--- a/feature/explore/src/main/java/kr/co/explore/ExploreViewModel.kt
+++ b/feature/explore/src/main/java/kr/co/explore/ExploreViewModel.kt
@@ -6,10 +6,11 @@ import kr.co.model.ExploreUiIntent
 import kr.co.model.ExploreUiState
 import kr.co.model.FileInfo
 import kr.co.ui.base.BaseMviViewModel
-import kr.co.util.readPDFOrDirectory
+import kr.co.util.FileManager
 
 internal class ExploreViewModel(
     private val recentRepository: RecentRepository,
+    private val fileManager: FileManager,
 ) :
     BaseMviViewModel<ExploreUiState, ExploreUiIntent, ExploreSideEffect>(ExploreUiState.INIT) {
 
@@ -26,7 +27,7 @@ internal class ExploreViewModel(
             copy(path = path)
         }
 
-        readPDFOrDirectory(path).partition { it.isDirectory }.let { (folders, files) ->
+        fileManager.readPDFOrDirectory(path).partition { it.isDirectory }.let { (folders, files) ->
             reduce {
                 copy(
                     folders = folders,
diff --git a/feature/explore/src/main/java/kr/co/util/FileManager.kt b/feature/explore/src/main/java/kr/co/util/FileManager.kt
new file mode 100644
index 0000000..1509207
--- /dev/null
+++ b/feature/explore/src/main/java/kr/co/util/FileManager.kt
@@ -0,0 +1,7 @@
+package kr.co.util
+
+import kr.co.model.FileInfo
+
+internal fun interface FileManager {
+    suspend fun readPDFOrDirectory(path: String): List<FileInfo>
+}
diff --git a/feature/explore/src/main/java/kr/co/util/FileManagerImpl.kt b/feature/explore/src/main/java/kr/co/util/FileManagerImpl.kt
new file mode 100644
index 0000000..78de762
--- /dev/null
+++ b/feature/explore/src/main/java/kr/co/util/FileManagerImpl.kt
@@ -0,0 +1,44 @@
+package kr.co.util
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kr.co.model.FileInfo
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.attribute.BasicFileAttributes
+import java.nio.file.attribute.FileTime
+import java.time.LocalDateTime
+import java.time.ZoneId
+
+internal class FileManagerImpl: FileManager {
+    override suspend fun readPDFOrDirectory(
+        path: String,
+    ): List<FileInfo> = withContext(Dispatchers.IO) {
+        File(path).listFiles()?.filter { !it.isHidden && (it.isDirectory || it.extension == PDF) }
+            ?.map {
+                val attributes = getFileAttributes(it)
+                FileInfo(
+                    name = it.name,
+                    path = it.path,
+                    type = FileInfo.Type.from(it.extension),
+                    isDirectory = it.isDirectory,
+                    size = it.length(),
+                    isHidden = it.isHidden,
+                    createdAt = attributes.creationTime().toLocalDateTime(),
+                    lastModified = attributes.lastModifiedTime().toLocalDateTime(),
+                )
+            } ?: emptyList()
+    }
+
+    private fun getFileAttributes(file: File): BasicFileAttributes =
+        Files.readAttributes(file.toPath(), BasicFileAttributes::class.java)
+
+    private fun FileTime.toLocalDateTime(): LocalDateTime =
+        LocalDateTime.ofInstant(this.toInstant(), ZoneId.systemDefault())
+
+    companion object {
+        internal const val DEFAULT_STORAGE = "/storage/emulated/0"
+
+        private const val PDF = "pdf"
+    }
+}
\ No newline at end of file
diff --git a/feature/explore/src/main/java/kr/co/util/FileUtil.kt b/feature/explore/src/main/java/kr/co/util/FileUtil.kt
deleted file mode 100644
index 747568b..0000000
--- a/feature/explore/src/main/java/kr/co/util/FileUtil.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package kr.co.util
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kr.co.model.FileInfo
-import java.io.File
-import java.nio.file.Files
-import java.nio.file.attribute.BasicFileAttributes
-import java.nio.file.attribute.FileTime
-import java.time.LocalDateTime
-import java.time.ZoneId
-
-internal suspend fun readPDFOrDirectory(
-    path: String,
-): List<FileInfo> = withContext(Dispatchers.IO) {
-    File(path).listFiles()?.filter { !it.isHidden && (it.isDirectory || it.extension == PDF) }
-        ?.map {
-            val attributes = getFileAttributes(it)
-            FileInfo(
-                name = it.name,
-                path = it.path,
-                type = FileInfo.Type.from(it.extension),
-                isDirectory = it.isDirectory,
-                size = it.length(),
-                isHidden = it.isHidden,
-                createdAt = attributes.creationTime().toLocalDateTime(),
-                lastModified = attributes.lastModifiedTime().toLocalDateTime(),
-            )
-        } ?: emptyList()
-}
-
-private fun getFileAttributes(file: File) : BasicFileAttributes =
-    Files.readAttributes(file.toPath(), BasicFileAttributes::class.java)
-
-private fun FileTime.toLocalDateTime(): LocalDateTime =
-    LocalDateTime.ofInstant(this.toInstant(), ZoneId.systemDefault())
-
-internal const val DEFAULT_STORAGE = "/storage/emulated/0"
-
-private const val PDF = "pdf"
\ No newline at end of file
diff --git a/feature/explore/src/test/kotlin/kr/co/explore/ExploreViewModelTest.kt b/feature/explore/src/test/kotlin/kr/co/explore/ExploreViewModelTest.kt
new file mode 100644
index 0000000..8b44da1
--- /dev/null
+++ b/feature/explore/src/test/kotlin/kr/co/explore/ExploreViewModelTest.kt
@@ -0,0 +1,120 @@
+package kr.co.explore
+
+import app.cash.turbine.test
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runTest
+import kr.co.model.ExploreSideEffect
+import kr.co.model.ExploreUiIntent
+import kr.co.model.FileInfo
+import kr.co.model.FileInfo.Type.PDF
+import kr.co.testing.repository.TestRecentRepository
+import kr.co.testing.rule.CoroutineTestRule
+import kr.co.util.FileManager
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.time.LocalDateTime
+import kotlin.test.assertEquals
+
+
+class ExploreViewModelTest {
+
+    @get: Rule
+    val coroutineTestRule = CoroutineTestRule()
+
+    private lateinit var viewModel: ExploreViewModel
+
+    private val recentRepository = TestRecentRepository()
+
+    @MockK
+    private lateinit var fileManager: FileManager
+
+    @Before
+    fun setup() {
+        MockKAnnotations.init(this)
+        viewModel = ExploreViewModel(recentRepository, fileManager)
+    }
+
+    @Test
+    fun `Given a path when Init intent is handled then state is updated`() = runTest {
+        val path = "/path"
+        val folders = listOf(
+            FOLDER_DUMMY
+        )
+        val files = listOf(
+            PDF_DUMMY,
+            PDF_DUMMY
+        )
+
+        coEvery { fileManager.readPDFOrDirectory(path) } returns folders + files
+
+        viewModel.handleIntent(ExploreUiIntent.Init(path))
+
+        viewModel.uiState.test {
+            val state = awaitItem()
+            assert(state.path == path)
+            assertEquals(state.files.size, files.size)
+            assertEquals(state.folders.size, folders.size)
+            assert(state.folders == folders)
+            assert(state.files == files)
+        }
+    }
+
+    @Test
+    fun `Given a file when ClickFile intent is handled then navigate to pdf`() = runTest {
+        val file = PDF_DUMMY
+
+        recentRepository.insert(file)
+
+        viewModel.handleIntent(ExploreUiIntent.ClickFile(file))
+
+        recentRepository.insert(file)
+
+        viewModel.sideEffect.test {
+            awaitItem().also {
+                assert(it is ExploreSideEffect.NavigateToPdf)
+                assert((it as ExploreSideEffect.NavigateToPdf).path == file.path)
+            }
+        }
+    }
+
+    @Test
+    fun `Given a folder when ClickFolder intent is handled then navigate to folder`() = runTest {
+        val folder = FOLDER_DUMMY
+
+        viewModel.handleIntent(ExploreUiIntent.ClickFolder(folder))
+
+        viewModel.sideEffect.test {
+            awaitItem().also {
+                assert(it is ExploreSideEffect.NavigateToFolder)
+                assert((it as ExploreSideEffect.NavigateToFolder).path == folder.path)
+            }
+        }
+    }
+
+    companion object {
+        val PDF_DUMMY = FileInfo(
+            name = "DUMMY.pdf",
+            path = "",
+            type = PDF,
+            isDirectory = false,
+            isHidden = false,
+            size = 0,
+            createdAt = LocalDateTime.now(),
+            lastModified = LocalDateTime.now()
+        )
+
+        val FOLDER_DUMMY = FileInfo(
+            name = "DUMMY",
+            path = "",
+            type = PDF,
+            isDirectory = true,
+            isHidden = false,
+            size = 0,
+            createdAt = LocalDateTime.now(),
+            lastModified = LocalDateTime.now()
+        )
+    }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 301f082..faf21c1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -40,6 +40,9 @@ lifecycleRuntimeKtx = "2.8.7"
 activityCompose = "1.9.3"
 composeBom = "2024.11.00"
 jetbrainsKotlinJvm = "1.9.0"
+mockk = "1.13.14"
+firebaseCrashlyticsBuildtools = "3.0.2"
+turbine = "1.2.0"
 
 [libraries]
 android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
@@ -49,8 +52,12 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
 androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
 androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
 
+mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
+mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
+
 kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
 kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
 
 koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
 koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
@@ -109,6 +116,8 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
 androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
 material = { group = "com.google.android.material", name = "material", version.ref = "material" }
 androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycleService" }
+firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
+turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
 
 [bundles]
 androidx = [
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b792150..5d0c267 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -38,3 +38,4 @@ include(":feature:explore")
 include(":feature:recent")
 include(":feature:bookmark")
 include(":feature:pdf")
+include(":core:testing")