Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔀 :: 테스트 커버리지 시각화 #312

Merged
merged 11 commits into from
Oct 23, 2024
14 changes: 8 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ subprojects {
implementation(Dependencies.JAVA_SERVLET)

/* test */
implementation(Dependencies.SPRING_TEST)
implementation(Dependencies.MOCKK)
implementation(Dependencies.KOTEST_RUNNER)
implementation(Dependencies.KOTEST_EXTENSION)
implementation(Dependencies.KOTEST_ASSERTIONS)
testImplementation(Dependencies.SPRING_TEST) {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation(Dependencies.MOCKK)
testImplementation(Dependencies.KOTEST_RUNNER)
testImplementation(Dependencies.KOTEST_EXTENSION)
testImplementation(Dependencies.KOTEST_ASSERTIONS)
}
}

Expand Down Expand Up @@ -62,4 +64,4 @@ allprojects {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
}
8 changes: 4 additions & 4 deletions buildSrc/src/main/kotlin/DependenciesVersions.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
object DependenciesVersions {
const val QUERYDSL = "5.0.0"
const val MOCKK = "1.12.0"
const val KOTEST_JUNIT = "5.3.2"
const val MOCKK = "1.13.8"
const val KOTEST_JUNIT = "5.4.2"
const val KOTEST_EXTENSION = "1.1.2"
const val KOTEST_ASSERTIONS = "5.5.5"
const val KOTEST_ASSERTIONS = "5.4.2"
const val SPRING_TRANSACTION = "5.3.11"
const val JACKSON_CORE = "2.13.4"
const val SPRING_TEST = "2.7.7"
Expand All @@ -17,4 +17,4 @@ object DependenciesVersions {
const val GAUTH_VERSION = "v2.0.0"
const val LOG_VERSION = "2.1.21"
const val FCM_VERSION = "8.1.0"
}
}
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/PluginVersions.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
object PluginVersions {
const val SPRING_BOOT_VERSION = "2.7.5"
const val DEPENDENCY_MANAGER_VERSION = "1.1.0"
const val JVM_VERSION = "1.7.22"
const val JVM_VERSION = "1.9.0"
const val SPRING_PLUGIN_VERSION = "1.7.22"
const val JPA_PLUGIN_VERSION = "1.6.21"
const val ALL_OPEN_VERSION = "1.6.21"
const val KAPT_VERSION = "1.7.10"
}
}
37 changes: 36 additions & 1 deletion goms-application/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
kotlin("plugin.allopen") version PluginVersions.ALL_OPEN_VERSION
id("jacoco")
}

repositories {
Expand All @@ -24,4 +25,38 @@ dependencies {
allOpen {
annotation(AllOpen.USECASE_WIHT_TRANSACTION)
annotation(AllOpen.USECASE_WIHT_READONLY_TRANSACTION)
}
}

jacoco {
toolVersion = "0.8.12"
}

tasks.test {
finalizedBy("jacocoTestReport") // 테스트 후에 JaCoCo 리포트 생성

jacoco {
isEnabled = true // JaCoCo가 활성화되어 있는지 확인
}
}

tasks.jacocoTestReport {
dependsOn(tasks.test) // 테스트 후에 JaCoCo 리포트 생성

reports {
xml.required.set(true)
html.required.set(true)
}

classDirectories.setFrom(
files(classDirectories.files.map {
fileTree(it) {
exclude("**/common/**")
exclude("**/data/**")
exclude("**/exception/**")
exclude("**/scheduler/**")
exclude("**/spi/**")
exclude("**/event/**")
}
})
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ class OutingUseCase(
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ class QueryAllAccountUseCase(
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.goms.v2.common
import com.goms.v2.domain.account.constant.Authority
import com.goms.v2.domain.account.constant.Gender
import com.goms.v2.domain.account.constant.Major
import com.goms.v2.domain.auth.EmailStatus
import com.goms.v2.domain.auth.data.event.SaveRefreshTokenEvent
import com.goms.v2.domain.late.data.dto.LateRankDto
import java.time.LocalDate
Expand All @@ -15,7 +16,7 @@ import kotlin.reflect.full.primaryConstructor

object AnyValueObjectGenerator {

inline fun <reified T : Any> anyValueObject(vararg pairs: Pair<String, Any>): T {
inline fun <reified T : Any> anyValueObject(vararg pairs: Pair<String, Any?>): T {
val parameterMap = mutableMapOf(*pairs)
val constructor = T::class.primaryConstructor!!

Expand Down Expand Up @@ -64,6 +65,7 @@ object AnyValueObjectGenerator {
Authority::class -> Authority.ROLE_STUDENT
Gender::class -> Gender.MAN
Major::class -> Major.SMART_IOT
EmailStatus::class -> EmailStatus.BEFORE_SIGNUP
LateRankDto::class -> LateRankDto(
UUID.randomUUID(),
String(),
Expand All @@ -85,4 +87,4 @@ object AnyValueObjectGenerator {
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.goms.v2.domain.account

import com.goms.v2.common.AnyValueObjectGenerator
import com.goms.v2.common.util.AccountUtil
import com.goms.v2.domain.account.data.dto.ChangePasswordDto
import com.goms.v2.domain.account.exception.DuplicatedNewPasswordException
import com.goms.v2.domain.account.exception.PasswordNotMatchException
import com.goms.v2.domain.account.usecase.ChangePasswordUseCase
import com.goms.v2.domain.auth.exception.AccountNotFoundException
import com.goms.v2.domain.auth.spi.PasswordEncoderPort
import com.goms.v2.repository.account.AccountRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import java.util.*

class ChangePasswordUseCaseTest: BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val accountRepository = mockk<AccountRepository>()
val passwordEncoderPort = mockk<PasswordEncoderPort>()
val accountUtil = mockk<AccountUtil>()
val changePasswordUseCase = ChangePasswordUseCase(accountRepository, passwordEncoderPort, accountUtil)

Given("ChangePasswordDto가 주어지면") {
val accountIdx = UUID.randomUUID()
val newPassword = "123"
val encodedPassword = "encodedPassword"
val account = spyk<Account>(AnyValueObjectGenerator.anyValueObject<Account>("idx" to accountIdx))
val changePasswordDto = AnyValueObjectGenerator.anyValueObject<ChangePasswordDto>("newPassword" to newPassword)

every { accountUtil.getCurrentAccountIdx() } returns accountIdx
every { accountRepository.findByIdOrNull(accountIdx) } returns account
every { passwordEncoderPort.isPasswordMatch(changePasswordDto.password, account.password) } returns true
every { passwordEncoderPort.isPasswordMatch(changePasswordDto.newPassword, account.password) } returns false
every { passwordEncoderPort.passwordEncode(changePasswordDto.newPassword) } returns encodedPassword
every { accountRepository.save(account) } returns account

When("비밀번호 변경 요청을 하면") {
changePasswordUseCase.execute(changePasswordDto)

Then("비밀번호가 변경된 상태로 Account가 저장되어야 한다.") {
verify(exactly = 1) { account.updatePassword(encodedPassword) }
verify(exactly = 1) { accountRepository.save(account) }
}
}

When("Account가 존재하지 않는다면") {
every { accountRepository.findByIdOrNull(accountIdx) } returns null

Then("AccountNotFoundException을 터쳐야 한다.") {
shouldThrow<AccountNotFoundException> {
changePasswordUseCase.execute(changePasswordDto)
}
}
}

When("기존 비밀번호가 일치하지 않으면") {
every { passwordEncoderPort.isPasswordMatch(changePasswordDto.password, account.password) } returns false

Then("PasswordNotMatchException이 터져야 한다.") {
shouldThrow<PasswordNotMatchException> {
changePasswordUseCase.execute(changePasswordDto)
}
}
}

When("새로운 비밀번호와 기존 비밀번호가 동일하면") {
every { passwordEncoderPort.isPasswordMatch(changePasswordDto.newPassword, account.password) } returns true

Then("DuplicatedNewPasswordException이 터져야 한다.") {
shouldThrow<DuplicatedNewPasswordException> {
changePasswordUseCase.execute(changePasswordDto)
}
}
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.goms.v2.domain.account

import com.goms.v2.common.AnyValueObjectGenerator
import com.goms.v2.common.util.AccountUtil
import com.goms.v2.domain.account.spi.S3UtilPort
import com.goms.v2.domain.account.usecase.DeleteImageUseCase
import com.goms.v2.domain.auth.exception.AccountNotFoundException
import com.goms.v2.repository.account.AccountRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import java.util.UUID

class DeleteImageUseCaseTest: BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val accountRepository = mockk<AccountRepository>()
val s3UtilPort = mockk<S3UtilPort>()
val accountUtil = mockk<AccountUtil>()
val deleteImageUseCase = DeleteImageUseCase(accountRepository, s3UtilPort, accountUtil)


Given("프로필 이미지가 존재할 때") {
val accountIdx = UUID.randomUUID()
val account = spyk<Account>(AnyValueObjectGenerator.anyValueObject<Account>("idx" to accountIdx))
val profileURL = account.profileUrl

every { accountUtil.getCurrentAccountIdx() } returns accountIdx
every { accountRepository.findByIdOrNull(accountIdx) } returns account
every { s3UtilPort.deleteImage(account.profileUrl.toString()) } returns Unit
every { accountRepository.save(account) } returns account

When("프로필 이미지 삭제 요청이 들어오면") {
deleteImageUseCase.execute()

Then("프로필 이미지를 지운 후에 Account가 저장되어야한다.") {
verify(exactly = 1) { s3UtilPort.deleteImage((profileURL.toString())) }
verify(exactly = 1) { account.resetProfileUrl(null) }
verify(exactly = 1) { accountRepository.save(account) }
}
}

When("Account가 존재하지 않는다면") {
every { accountRepository.findByIdOrNull(accountIdx) } returns null

Then("AccountNotFoundException이 터져야 한다.") {
shouldThrow<AccountNotFoundException> {
deleteImageUseCase.execute()
}
}
}

}
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import com.goms.v2.repository.late.LateRepository
import com.goms.v2.repository.outing.OutingBlackListRepository
import com.goms.v2.repository.outing.OutingRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk

class QueryAccountProfileUseCaseTest: BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val accountUtil = mockk<AccountUtil>()
val lateRepository = mockk<LateRepository>()
val outingRepository = mockk<OutingRepository>()
Expand Down Expand Up @@ -52,4 +54,4 @@ class QueryAccountProfileUseCaseTest: BehaviorSpec({
}
}
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.goms.v2.domain.account

import com.goms.v2.common.AnyValueObjectGenerator
import com.goms.v2.domain.account.spi.S3UtilPort
import com.goms.v2.domain.account.usecase.UpdateImageUseCase
import com.goms.v2.repository.account.AccountRepository
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import org.springframework.mock.web.MockMultipartFile
import java.nio.charset.StandardCharsets
import java.util.*

class UpdateImageUseCaseTest: BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val accountRepository = mockk<AccountRepository>()
val s3UtilPort = mockk<S3UtilPort>()
val updateImageUseCase = UpdateImageUseCase(accountRepository, s3UtilPort)

Given("multipart image가 주어질 때") {
val imageBytes = "image content".toByteArray(StandardCharsets.UTF_8)
val image = MockMultipartFile("File", "image.png", "image/png", imageBytes)

val imageURL = ""
val accountIdx = UUID.randomUUID()
val account = spyk<Account>(AnyValueObjectGenerator.anyValueObject<Account>("idx" to accountIdx))

every { s3UtilPort.validImage(image) } returns account
every { s3UtilPort.deleteImage(account.profileUrl.toString()) } returns Unit
every { s3UtilPort.upload(image) } returns imageURL
every { accountRepository.save(account) } returns account

When("프로필 이미지 수정 요청을 하면") {
updateImageUseCase.execute(image)

Then("account가 저장되어야 한다.") {
verify(exactly = 1) { account.updateProfileUrl(imageURL) }
verify(exactly = 1) { accountRepository.save(account) }
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import com.goms.v2.domain.auth.exception.AccountNotFoundException
import com.goms.v2.domain.auth.spi.PasswordEncoderPort
import com.goms.v2.repository.account.AccountRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.springframework.context.ApplicationEventPublisher

class UpdatePasswordUseCaseTest: BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val accountRepository = mockk<AccountRepository>()
val authenticationValidator = mockk<AuthenticationValidator>()
val passwordEncoderPort = mockk<PasswordEncoderPort>()
Expand Down Expand Up @@ -58,7 +60,6 @@ class UpdatePasswordUseCaseTest: BehaviorSpec({
}
}
When("이미 사용중인 비밀번호로 변경하면") {
every { accountRepository.findByEmail(passwordDto.email) } returns account
every { passwordEncoderPort.isPasswordMatch(passwordDto.newPassword, account.password) } returns true

Then("DuplicatedNewPasswordException 이 터져야 한다.") {
Expand All @@ -68,4 +69,4 @@ class UpdatePasswordUseCaseTest: BehaviorSpec({
}
}
}
})
})
Loading
Loading