Skip to content

Commit

Permalink
test: 아키텍처 테스트 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
belljun3395 committed Dec 5, 2024
1 parent bb59011 commit b58a090
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 1 deletion.
100 changes: 100 additions & 0 deletions api/src/test/kotlin/com/few/api/domain/ApiDomainArchitectureSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.few.api.domain

import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.library.Architectures.layeredArchitecture
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class ApiDomainArchitectureSpec {
companion object {
var apiDomainClasses: JavaClasses? = null

@BeforeAll
@JvmStatic
fun setup() {
apiDomainClasses = ClassFileImporter().importPackages(
"com.few.api.domain.article",
"com.few.api.domain.log",
"com.few.api.domain.member",
"com.few.api.domain.problem",
"com.few.api.domain.subscription",
"com.few.api.domain.workbook"
)
}
}

@Test
fun `Controller 레이어는 다른 레이어에서 접근할 수 없어야 한다`() {
val rule = layeredArchitecture()
.layer("controller").definedBy("..controller..")
.whereLayer("controller").mayNotBeAccessedByAnyLayer()

rule.check(apiDomainClasses)
}

@Test
fun `Usecase 레이어는 Controller 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("usecase").definedBy("..usecase..")
.layer("controller").definedBy("..controller..")
.whereLayer("usecase").mayOnlyBeAccessedByLayers("controller")

rule.check(apiDomainClasses)
}

@Test
fun `Service 레이어는 Usecase와 Event 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("service").definedBy("..service..")
.layer("usecase").definedBy("..usecase..")
.layer("event").definedBy("..event..")
.whereLayer("service").mayOnlyBeAccessedByLayers("usecase", "event")

rule.check(apiDomainClasses)
}

@Test
fun `Repo 레이어는 Usecase, Service, Event 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("repo").definedBy("..repo..")
.layer("usecase").definedBy("..usecase..")
.layer("service").definedBy("..service..")
.layer("event").definedBy("..event..")
.whereLayer("repo").mayOnlyBeAccessedByLayers("usecase", "service", "event")

rule.check(apiDomainClasses)
}

@Test
fun `Event 레이어는 Usecase 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("event").definedBy("..event..")
.layer("usecase").definedBy("..usecase..")
.whereLayer("event").mayOnlyBeAccessedByLayers("usecase")

rule.check(apiDomainClasses)
}

@Test
fun `Email 레이어는 UseCase, Service 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("email").definedBy("..email..")
.layer("usecase").definedBy("..usecase..")
.layer("service").definedBy("..service..")
.whereLayer("email").mayOnlyBeAccessedByLayers("usecase", "service")

rule.check(apiDomainClasses)
}

@Test
fun `Client 레이어는 UseCase, Event 레이어에서만 접근 가능하다`() {
val rule = layeredArchitecture()
.layer("client").definedBy("..client..")
.layer("usecase").definedBy("..usecase..")
.layer("event").definedBy("..event..")
.whereLayer("client").mayOnlyBeAccessedByLayers("usecase", "event")

rule.check(apiDomainClasses)
}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ subprojects {
/** test **/
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.mockk:mockk:${DependencyVersion.MOCKK}")
testImplementation("com.tngtech.archunit:archunit-junit5:${DependencyVersion.ARCH_UNIT_JUNIT5}")

/** kotest */
testImplementation("io.kotest:kotest-runner-junit5:${DependencyVersion.KOTEST}")
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/DependencyVersion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object DependencyVersion {
const val KOTEST_EXTENSION = "1.1.3"
const val COROUTINE_TEST = "1.8.0"
const val TEST_CONTAINER = "1.19.8"
const val ARCH_UNIT = "1.3.0"
const val ARCH_UNIT_JUNIT5 = "0.22.0"

/** docs */
const val ASCIIDOCTOR = "3.3.2"
Expand Down
34 changes: 34 additions & 0 deletions repo/src/test/kotlin/architecture/RepoModuleArchitectureSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package architecture

import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Configuration
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.lang.ArchRule
import org.junit.jupiter.api.BeforeAll

class RepoModuleArchitectureSpec {
companion object {
var repoClasses: JavaClasses? = null

@BeforeAll
@JvmStatic
fun setup() {
repoClasses = ClassFileImporter().importPackages("repo")
}
}

@Test
fun `repo 모듈의 설정 클래스는 config 패키지에 존재합니다`() {
val rule: ArchRule = classes()
.that()
.resideInAPackage("repo")
.and().haveNameNotMatching(".*Companion*.")
.and().areAnnotatedWith(Configuration::class.java)
.should()
.resideInAPackage("repo.config")

rule.check(repoClasses)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package architecture

import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Configuration

class SecurityModuleArchitectureSpec {
companion object {
var securityClasses: JavaClasses? = null

@BeforeAll
@JvmStatic
fun setup() {
securityClasses = ClassFileImporter().importPackages("security")
}
}

@Test
fun `security 모듈의 설정 클래스는 config 패키지에 존재합니다`() {
val rule = classes()
.that()
.resideInAPackage("security")
.and().haveNameNotMatching(".*Companion*.")
.and().areAnnotatedWith(Configuration::class.java)
.should()
.resideInAPackage("security.config")

rule.check(securityClasses)
}
}
154 changes: 154 additions & 0 deletions storage/src/test/kotlin/architecture/StorageModuleArchitectureSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package architecture

import com.tngtech.archunit.base.DescribedPredicate
import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Configuration
import storage.GetPreSignedObjectUrlProvider
import storage.PutObjectProvider
import storage.RemoveObjectProvider

class StorageModuleArchitectureSpec {
companion object {
var storageClasses: JavaClasses? = null

@BeforeAll
@JvmStatic
fun setup() {
storageClasses = ClassFileImporter().importPackages("storage")
}
}

@Test
fun `storage 모듈의 설정 클래스는 config 패키지에 존재합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage")
.and().haveNameNotMatching(".*Companion*.")
.and().resideOutsideOfPackages("storage.document", "storage.image")
.and().areAnnotatedWith(Configuration::class.java)
.should()
.resideInAPackage("storage.config")

rule.check(storageClasses)
}

@Test
fun `storage 모듈의 object의 url을 제공하기 위한 provider는 GetPreSignedObjectUrlProvider를 구현해야합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.document")
.or().resideInAPackage("storage.image")
.and().haveSimpleNameStartingWith("Get")
.and().haveSimpleNameEndingWith("Provider")
.and().haveNameNotMatching(".*Companion*.")
.and().areNotInterfaces()
.should()
.implement(
DescribedPredicate.describe(
"GetPreSignedObjectUrlProvider를 구현해야합니다"
) { clazz ->
clazz.interfaces.javaClass.interfaces.contains(GetPreSignedObjectUrlProvider::class.java)
}
)

rule.check(storageClasses)
}

@Test
fun `storage 모듈의 object를 수정하기 위한 provider는 PutObjectProvider를 구현해야합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.document")
.or().resideInAPackage("storage.image")
.and().haveSimpleNameStartingWith("Put")
.and().haveSimpleNameEndingWith("Provider")
.and().haveNameNotMatching(".*Companion*.")
.and().areNotInterfaces()
.should()
.implement(
DescribedPredicate.describe(
"PutObjectProvider를 구현해야합니다"
) { clazz ->
clazz.interfaces.javaClass.interfaces.contains(PutObjectProvider::class.java)
}
)

rule.check(storageClasses)
}

@Test
fun `storage 모듈의 object를 삭제하기 위한 provider는 RemoveObjectProvider를 구현해야합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.document")
.or().resideInAPackage("storage.image")
.and().haveSimpleNameStartingWith("Remove")
.and().haveSimpleNameEndingWith("Provider")
.and().haveNameNotMatching(".*Companion*.")
.and().areNotInterfaces()
.should()
.implement(
DescribedPredicate.describe(
"RemoveObjectProvider를 구현해야합니다"
) { clazz ->
clazz.interfaces.javaClass.interfaces.contains(RemoveObjectProvider::class.java)
}
)

rule.check(storageClasses)
}

@Test
fun `client 패키지의 client 클래스는 provider 패키지의 provider 클래스에서만 사용되어야 합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.*.client")
.and().haveSimpleNameEndingWith("Client")
.and().haveNameNotMatching(".*Companion*.")
.should()
.onlyBeAccessed().byAnyPackage(
"storage.*.client",
"storage.*.provider.*",
"storage.*.config"
)

rule.check(storageClasses)
}

@Nested
inner class DocumentArchitectureSpec {
@Test
fun `document 패키지의 설정 클래스는 config 패키지에 존재합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.document")
.and().haveNameNotMatching(".*Companion*.")
.and().areAnnotatedWith(Configuration::class.java)
.should()
.resideInAPackage("storage.document.config")

rule.check(storageClasses)
}
}

@Nested
inner class ImageArchitectureSpec {
@Test
fun `image 패키지의 설정 클래스는 config 패키지에 존재합니다`() {
val rule = classes()
.that()
.resideInAPackage("storage.image")
.and().haveNameNotMatching(".*Companion*.")
.and().areAnnotatedWith(Configuration::class.java)
.should()
.resideInAPackage("storage.image.config")

rule.check(storageClasses)
}
}
}
Loading

0 comments on commit b58a090

Please sign in to comment.