Skip to content

Commit

Permalink
Merge pull request #312 from team-haribo/feature/311_jacoco
Browse files Browse the repository at this point in the history
🔀 :: 테스트 커버리지 시각화
  • Loading branch information
yoontaebin123 authored Oct 23, 2024
2 parents 3424a15 + 64df5b6 commit c271ef1
Show file tree
Hide file tree
Showing 43 changed files with 606 additions and 76 deletions.
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

0 comments on commit c271ef1

Please sign in to comment.