간혹 통합테스트를 작성하다보면 아래와 같은 생각이 들곤 한다.
"비즈니스 로직이 의도한대로 작동되는지 테스트는 할 수 있겠어.
하지만 로직 외에 아키텍처 의존성을 테스트할 수는 없을까?"
Archunit 라이브러리를 이용하면 아키텍처 의존성, 클래스명 등을 테스트할 수 있다!
참고로 Archunit Guide는 아래 링크에서 확인할 수 있다.
https://www.archunit.org/userguide/html/000_Index.html#_introduction
Archunit을 사용하면 좋을 때
- 클래스명을 컨벤션에 맞게 작성했는지 확인하고 싶을 때
- 아키텍처 의존성이 올바른지 지속적 통합 등으로 체크하고 싶을 때
참고로 가이드에서의 4. What to check 에는 아래 항목들이 존재한다.
- 4.1. Package Dependency Checks
- 4.2. Class Dependency Checks
- 4.3. Class and Package Containment Checks
- 4.4. Inheritance Checks
- 4.5. Annotation Checks
- 4.6. Layer Checks
- 4.7. Cycle Checks
Archunit 사용법
https://mvnrepository.com/artifact/com.tngtech.archunit/archunit-junit5 를 참고하여 자신이 사용하려는 시점에 맞는 버전, 환경(maven, gradle, gradle(kotlin) 등)에 맞게 라이브러리를 적용하자.
build.gradle
1
2
3
4
|
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.0.1'
}
|
cs |
1-1. 잘못된 의존성 테스트
JUnit과 함께 사용할 경우 @AnalyzeClasses 어노테이션을 통해 패키지명을 인식시켜주면, ArchRule과 @ArchTest 만으로 테스트가 가능하다.
아래 코드는 mocacong.server.repository 패키지의 클래스들이 mocacong.server.controller 내에 하나라도 존재하면 테스트가 실패하도록 한 것이다. (controller는 repository를 의존해서는 안된다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package mocacong.server.architectures;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "mocacong.server")
public class DependencyTest {
@ArchTest
public static final ArchRule repository는_controller에_존재해서는_안된다 = noClasses()
.that()
.resideInAPackage("..repository..")
.should()
.dependOnClassesThat()
.resideInAPackage("..controller..");
}
|
cs |
패키지명 실수를 하지 않도록 주의하자.
아, 참고로 main 뿐만 아니라 test 패키지 내에 있는 mocacong.server.repository 도 체크하므로 주의하자.
1-2. 올바른 의존성 테스트
아래 코드는 mocacong.server.repository 패키지 내의 클래스들이 mocacong.server.repository, mocacong.server.service 둘 중 하나의 패키지에 모두 속하는지 확인하는 테스트 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package mocacong.server.architectures;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@AnalyzeClasses(packages = "mocacong.server")
public class DependencyTest {
@ArchTest
public static final ArchRule repository는_repository_또는_service에_존재한다 = classes()
.that()
.resideInAPackage("..repository..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("..service..", "..repository..");
}
|
cs |
noClasses() 에서 classes()로, resideInAPackage()에서 resideInAnyPackage()로 바꼈음에 주목하면 된다.
또, resideInAPackage()는 인자로 여러 개를 넣을 수 없지만, resideInAnyPackage()는 인자로 여러 개를 넣을 수 있다.
그 외에 다양한 메서드들이 있으니 잘 이용하도록 하자.
2. 클래스명 테스트
의존성, 사이클뿐만 아니라 클래스명 컨벤션도 테스트할 수 있다.
아래 코드는 mocacong.server.repository 패키지 내의 클래스들이 repository 라는 네이밍을 포함하고 있는지 확인하는 테스트 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package mocacong.server.architectures;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
@AnalyzeClasses(packages = "mocacong.server")
public class DependencyTest {
@ArchTest
public static final ArchRule repository_클래스명은_Repository를_포함한다 = classes()
.that()
.resideInAPackage("..repository..")
.should()
.haveSimpleNameContaining("Repository");
}
|
cs |
만약 haveSimpleNameContaining() 대신 haveSimpleNameEndingWith()을 쓴다면?
(rule에 포함된 네이밍으로 반드시 클래스명이 끝나야 테스트가 통과하도록)
테스트가 실패한다!
main 패키지 내의 mocacong.server.repository 패키지 클래스명들은 모두 Repository로 끝나는데 왜 테스트가 실패할까?
위에서도 작성했다시피, main 패키지뿐만 아니라 test 패키지 등. 모든 패키지의 mocacong.server.repository을 체크하기 때문이다! 해당 프로젝트의 test 패키지 내에서의 mocacong.server.repository 클래스명들은 RepositoryTest로 끝나기 때문이다.
그래서 haveSimpleNameEndingWith() 대신 haveSimpleNameContaining()을 쓴 것이다.
그 외에 다양한 메서드들이 있으니 상황에 맞게 잘 이용해보자.
만약 ArchTest를 Ignore하고 싶다면?
@ArchIgnore 어노테이션으로 무시할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package mocacong.server.architectures;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchIgnore;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
@AnalyzeClasses(packages = "mocacong.server")
public class DependencyTest {
@ArchTest
@ArchIgnore
public static final ArchRule repository_클래스명은_Repository를_포함한다 = classes()
.that()
.resideInAPackage("..repository..")
.should()
.haveSimpleNameEndingWith("Repository");
}
|
cs |
해당 테스트는 무시한다.
마치며
사실 return type 체크도 https://github.com/TNG/ArchUnit/issues/418 를 보면 가능할 듯한데, 가이드 문서에는 별도 언급이 없는 듯하여 생략하였다. ArchUnit 라이브러리는 주로 아키텍처 의존성 테스트를 위해 사용되는 듯하다.
아키텍처 내에 사이클이 존재하는지, 컨벤션에 맞게 상속 또는 어노테이션을 사용했는지 테스트하기 좋을 듯하다.
나도 우연히 알게 된 라이브러리라, 추후에 `이런 게 있구나~` 하면서 기록해보았기 때문에 많은 내용을 담진 못하였다. 더 많은 내용은archunit.org 홈페이지의 userguide (아래 참고에 첨부) 를 참고하는 것을 권장한다.
TMI지만 우리 팀에선 안 쓰는 듯하다.
하지만 사내에서 알게 된 키워드는 맞다. 다른 팀에서 조금씩 쓰는 듯.
참고
- https://medium.com/free-code-camp/java-archunit-testing-the-architecture-a09f089585be
- https://www.archunit.org/userguide/html/000_Index.html#_introduction
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[Spring] Apache POI 엑셀 다운 vs opencsv CSV 다운 로직 및 성능 비교하기 (feat. parallelStream) (2) | 2023.09.19 |
---|---|
[230826] 유스콘 2023 컨퍼런스 후기 (51) | 2023.08.27 |
[Spring] Elasticache Redis 캐싱과 테스트 코드를 이용한 성능 개선 (2) | 2023.05.29 |
[Spring] logback 로깅 전략 및 민감정보 마스킹 로그 처리를 하자 (2) | 2023.05.12 |
[Spring] 이벤트 발행 및 @TransactionalEventListener을 이용한 의존성 줄이기 (2) | 2023.04.25 |