Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/WSS_PR_builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ jobs:

runs-on: ubuntu-latest

permissions:
contents: read
pull-requests: write
issues: write

steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -54,12 +59,22 @@ jobs:
echo amplitude.key=\"$AMPLITUDE_KEY\" >> ./local.properties

- name: Run Ktlint
if: github.event_name == 'pull_request'
uses: ScaCap/action-ktlint@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
android: true
fail_on_error: true
reporter: github-pr-review

- name: Build with Gradle
run: ./gradlew build
# 2. PR용: 모든 모듈의 로직을 검증하되, 빌드(컴파일)는 앱 중심으로 효율화
- name: Test & Assemble (PR)
if: github.event_name == 'pull_request'
# 1. 모든 모듈의 유닛 테스트 실행 (로직 검증)
# 2. 앱 모듈 디버그 빌드 실행 (의존성 모듈 자동 포함 빌드)
run: ./gradlew testDebugUnitTest :app:assembleDebug --scan

# 3. Merge(Push)용: 전체 검증 (안전장치)
- name: Full Build (Push)
if: github.event_name == 'push'
run: ./gradlew build --scan
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ dependencies {

// 테스트 관련 라이브러리
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.coroutines.test)
androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.espresso.core)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.into.websoso.domain.usecase

import com.into.websoso.data.model.ExploreResultEntity
import com.into.websoso.data.repository.NovelRepository
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test

class GetSearchedNovelsUseCaseTest {
private lateinit var getSearchedNovelsUseCase: GetSearchedNovelsUseCase
private val novelRepository: NovelRepository = mockk(relaxed = true)

private val dummyExploreResultEntity = ExploreResultEntity(
resultCount = 0L,
isLoadable = true,
novels = emptyList(),
)

@Before
fun setUp() {
getSearchedNovelsUseCase = GetSearchedNovelsUseCase(novelRepository)
coEvery {
novelRepository.fetchNormalExploreResult(
any(),
any(),
any(),
)
} returns dummyExploreResultEntity
}

@Test
fun `처음 검색하면 페이지는 0, 사이즈는 20으로 레포지토리에 요청한다`() =
runTest {
// when
getSearchedNovelsUseCase("웹소설")

// then

coVerify(exactly = 1) {
novelRepository.fetchNormalExploreResult(
searchWord = "웹소설",
page = 0,
size = 20,
)
}
}

@Test
fun `같은 검색어로 다시 검색하면 페이지는 1, 사이즈는 10으로 레포지토리에 요청한다`() =
runTest {
// when
getSearchedNovelsUseCase("웹소설") // 0페이지 20개
getSearchedNovelsUseCase("웹소설") // 1페이지 10개

// then
coVerify(exactly = 1) {
novelRepository.fetchNormalExploreResult(
searchWord = "웹소설",
page = 1,
size = 10,
)
}
}

@Test
fun `다른 검색어로 검색하면 캐시를 지우고 페이지는 0으로 레포지토리에 요청한다`() =
runTest {
// when
getSearchedNovelsUseCase("웹소설") // clear 1회
clearMocks(novelRepository, answers = false)
getSearchedNovelsUseCase("새로운 웹소설")

// then
coVerify(exactly = 1) {
novelRepository.clearCachedNormalExploreResult()
novelRepository.fetchNormalExploreResult(
searchWord = "새로운 웹소설",
page = 0,
size = 20,
)
}
}
}
1 change: 1 addition & 0 deletions domain/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ android {
dependencies {
implementation(projects.core.common)
implementation(projects.data.library)
testImplementation(libs.junit)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 앞으로 테스트를 어느범위까지 적용시킬 생각이신가요?
범위에 따라 테스트 의존성을 플러그인에 추가해도 좋을 것 같아서요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 도메인 레이어 모델과 하나의 UseCase에 대해서만 로컬 유닛 테스트를 작성한 상태입니다.
추후 테스트 범위가 확장될 경우에는 의존성 관리 방식도 함께 정리하여, 모듈별 개별 추가보다는 공통 플러그인으로 관리하는 방향을 고려하겠습니다..!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.into.websoso.domain.library.model

import com.into.websoso.domain.library.model.AttractivePoints.Companion.toAttractivePoints
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class AttractivePointsTest {
@Test
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 이 아래로 gwt주석이 없는데, 유즈케이스 테스트랑 코드 스타일이 통일되면 좋을 것 같아요 주석이 다 있거나 아예 다 없거나!
개인적으론 유즈케이스에서 gwt로 문단나눠주신게 더 잘 읽혔습니다!

Copy link
Copy Markdown
Contributor Author

@devfeijoa devfeijoa Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 없는 테스트 케이스 모두 gwt으로 통일하였습니다!
1c05a89

fun `기본 상태에서는 아무 항목도 선택되지 않는다`() {
// given
val points = AttractivePoints()

// then
assertFalse(points.isSelected)
assertTrue(points.selectedAttractivePoints.isEmpty())
assertTrue(points.selectedLabels.isEmpty())
assertTrue(points.selectedKeys.isEmpty())

for (point in AttractivePoint.entries) {
assertFalse(points[point])
}
}

@Test
fun `항목을 선택하면 해당 항목만 선택 상태가 된다`() {
// given
val points = AttractivePoints()

// when
val selected = points.set(AttractivePoint.WORLDVIEW)

// then
assertTrue(selected[AttractivePoint.WORLDVIEW])
assertTrue(selected.isSelected)
assertEquals(listOf("세계관"), selected.selectedLabels)
assertEquals(listOf("worldview"), selected.selectedKeys)
}

@Test
fun `같은 항목을 다시 선택하면 선택이 해제된다`() {
// given
val points = AttractivePoints()

// when
val toggled = points
.set(AttractivePoint.WORLDVIEW)
.set(AttractivePoint.WORLDVIEW)

// then
assertFalse(toggled[AttractivePoint.WORLDVIEW])
assertFalse(toggled.isSelected)
}

@Test
fun `문자열 키 목록을 통해 선택 상태를 생성할 수 있다`() {
// given
val keys = listOf("worldview", "character", "unknown")

// when
val points = keys.toAttractivePoints()

// then
assertTrue(points[AttractivePoint.WORLDVIEW])
assertTrue(points[AttractivePoint.CHARACTER])
assertFalse(points[AttractivePoint.MATERIAL])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.into.websoso.domain.library.model

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class NovelRatingTest {
@Test
fun `기본 평점은 선택되지 않은 상태이다`() {
// given
val rating = NovelRating()

// then
assertEquals(Rating.DEFAULT, rating.rating)
assertFalse(rating.isSelected)
}

@Test
fun `평점을 설정하면 선택 상태가 된다`() {
// given
val input = 4.0f

// when
val rating = NovelRating.from(input)

// then
assertTrue(rating.isSelected)
assertEquals(Rating.FOUR, rating.rating)
}

@Test
fun `같은 평점을 다시 설정하면 기본 상태로 돌아간다`() {
// given
val rating = NovelRating.from(3.0f)

// when
val toggled = rating.set(Rating.THREE)

// then
assertEquals(Rating.DEFAULT, toggled.rating)
assertFalse(toggled.isSelected)
}

@Test
fun `평점은 근사값 비교로 판단된다`() {
// given
val rating = NovelRating.from(4.0f)

// when
val result = rating.isCloseTo(Rating.from(4.00001f))

// then
assertTrue(result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.into.websoso.domain.library.model

import org.junit.Assert.assertEquals
import org.junit.Test

class RatingTest {
@Test
fun `정확한 값은 대응되는 평점으로 변환된다`() {
// when
val default = Rating.from(0.0f)
val one = Rating.from(1.0f)
val threePointFive = Rating.from(3.5f)
val fourPointEight = Rating.from(4.8f)
val five = Rating.from(5.0f)

// then
assertEquals(Rating.DEFAULT, default)
assertEquals(Rating.ONE, one)
assertEquals(Rating.THREE_POINT_FIVE, threePointFive)
assertEquals(Rating.FOUR_POINT_EIGHT, fourPointEight)
assertEquals(Rating.FIVE, five)
}

@Test
fun `근사값도 가까운 평점으로 변환된다`() {
// when
val four = Rating.from(4.00001f)
val fourPointEight = Rating.from(4.79999f)

// then
assertEquals(Rating.FOUR, four)
assertEquals(Rating.FOUR_POINT_EIGHT, fourPointEight)
}

@Test
fun `매칭되지 않는 값은 기본 평점으로 처리된다`() {
// when
val negative = Rating.from(-1.0f)
val unmatched = Rating.from(4.7f)
val overflow = Rating.from(100.0f)

// then
assertEquals(Rating.DEFAULT, negative)
assertEquals(Rating.DEFAULT, unmatched)
assertEquals(Rating.DEFAULT, overflow)
}
Comment on lines +36 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

// when 주석 들여쓰기 누락

Line 37의 // when 주석이 들여쓰기 없이 컬럼 0에 위치해 있어, 나머지 테스트 메서드들과 스타일이 불일치합니다.

🛠️ 수정 제안
     fun `매칭되지 않는 값은 기본 평점으로 처리된다`() {
-// when
+        // when
         val negative = Rating.from(-1.0f)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun `매칭되지 않는 값은 기본 평점으로 처리된다`() {
// when
val negative = Rating.from(-1.0f)
val unmatched = Rating.from(4.7f)
val overflow = Rating.from(100.0f)
// then
assertEquals(Rating.DEFAULT, negative)
assertEquals(Rating.DEFAULT, unmatched)
assertEquals(Rating.DEFAULT, overflow)
}
fun `매칭되지 않는 값은 기본 평점으로 처리된다`() {
// when
val negative = Rating.from(-1.0f)
val unmatched = Rating.from(4.7f)
val overflow = Rating.from(100.0f)
// then
assertEquals(Rating.DEFAULT, negative)
assertEquals(Rating.DEFAULT, unmatched)
assertEquals(Rating.DEFAULT, overflow)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@domain/library/src/test/java/com/into/websoso/domain/library/model/RatingTest.kt`
around lines 36 - 46, The test method `매칭되지 않는 값은 기본 평점으로 처리된다` has the `//
when` comment left-aligned at column 0; fix the formatting by indenting the `//
when` comment to match the other comments in this file (align with the
surrounding test comments in RatingTest.kt), keeping the comment directly above
the block that calls Rating.from and ensuring consistent indentation style for
this test method.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.into.websoso.domain.library.model

import com.into.websoso.domain.library.model.ReadStatuses.Companion.toReadStatuses
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class ReadStatusesTest {
@Test
fun `기본 상태에서는 아무 상태도 선택되지 않는다`() {
// given
val statuses = ReadStatuses()

// then
assertFalse(statuses.isSelected)
assertTrue(statuses.selectedKeys.isEmpty())
assertTrue(statuses.selectedLabels.isEmpty())

for (status in ReadStatus.entries) {
assertFalse(statuses[status])
}
}

@Test
fun `읽기 상태를 선택하면 해당 상태가 선택된다`() {
// given
val statuses = ReadStatuses()

// when
val selected = statuses.set(ReadStatus.WATCHING)

// then
assertTrue(selected[ReadStatus.WATCHING])
assertTrue(selected.isSelected)
assertEquals(listOf("WATCHING"), selected.selectedKeys)
}

@Test
fun `같은 읽기 상태를 다시 선택하면 선택이 해제된다`() {
// given
val statuses = ReadStatuses()

// when
val toggled = statuses
.set(ReadStatus.WATCHING)
.set(ReadStatus.WATCHING)

// then
assertFalse(toggled[ReadStatus.WATCHING])
assertFalse(toggled.isSelected)
}

@Test
fun `문자열 키 목록으로 읽기 상태를 생성할 수 있다`() {
// given
val keys = listOf("WATCHING", "QUIT", "UNKNOWN")

// when
val statuses = keys.toReadStatuses()

// then
assertTrue(statuses[ReadStatus.WATCHING])
assertTrue(statuses[ReadStatus.QUIT])
assertFalse(statuses[ReadStatus.WATCHED])
}
}
Loading
Loading