이번 포스팅에선 아키텍처 테스트 라이브러리인 ArchUnit에 대해 알아보겠습니다.
아키텍처 테스트란?
아키텍처 테스트는 프로젝트가 정해놓은 아키텍처를 제대로 따르고 있는지 확인하는 것을 말합니다.
ArchUnit이란?
ArchUnit는 다양한 아키텍처 체크를 할 수 있는 메소드를 제공합니다.
1) 패키지 의존성 체크
2) 클래스 의존성 체크
3) 패키지 네이밍 룰 체크
4) 구현체 네이밍 룰 체크
5) 구현체 어노테이션 체크
6) 레이어 체크
7) 순환구조 체크
추가적인 내용은 공식 홈페이지 가이드를 참고하시길 바랍니다.
ArchUnit 설정
ArchUnit 의존성 추가
//Maven
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.17.0</version>
<scope>test</scope>
</dependency>
//Gradle
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:0.17.0'
}
ArchUnit 테스트 방법
ArchUnit은 jUnit5에서 지원해주기 때문에 손쉽게 사용할 수 있습니다.
1) 클래스에 @AnalyzeClasses(packages = "테스트할 패키지"), 변수엔 @ArchTest 어노테이션을 붙혀준다.
2) 어노테이션을 다 붙힌 후 검증할 패키지 조건을 정의해준다.
ArchUnit 테스트 조건은 영어 문장을 하나 만든다고 생각하시면 됩니다. (자세한 옵션은 공식 홈페이지를 참고해주세요.)
테스트 조건을 만드는 방법이 매우 간단하고 직관적입니다.
// soruce 패키지에 있는 어떤 클래스도 foo 클래스에 의존하면 안된다.
noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..");
1), 2)를 다 하면 아래와 같은 테스트 코드가 만들어집니다. 바로 예제를 보겠습니다.
@AnalyzeClasses(packages = "com.effortguy")
public class PackageDependency {
@ArchTest
ArchRule archRule = noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..");
@ArchTest
ArchRule archRule2 = classes().that().resideInAPackage("..target..")
.should().onlyHaveDependentClassesThat().resideInAPackage("..source..");
}
ArchUnit 테스트
위에서 언급한 방법 중 1~5번까지만 테스트를 해보겠습니다.
1) 패키지 의존성 체크
시나리오
① source 패키지에 있는 클래스는 foo 패키지를 의존하고 있으면 안 된다.
② target 패키지에 있는 클래스는 source 패키지만 의존하고 있어야 한다.
테스트
아래와 같이 패키지를 쭉 만들어줍니다.
Source에만 코드를 추가해줍니다.
public class Source {
Foo foo; // ① 위반
Target target;
}
테스트 코드를 작성합니다.
package com.effortguy.perftest.archunit;
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 = "com.effortguy")
public class PackageDependency {
@ArchTest // ①
ArchRule archRule = noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..");
@ArchTest // ②
ArchRule archRule2 = classes().that().resideInAPackage("..target..")
.should().onlyHaveDependentClassesThat().resideInAPackage("..source..");
}
결과
① source 패키지에 있는 클래스는 foo 패키지를 의존하고 있으면 안 된다. - source가 foo를 의존하고 있기에 실패
② target 패키지에 있는 클래스는 source 패키지만 의존하고 있어야 한다. - 성공
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..source..' should depend on classes that reside in a package '..foo..'' was violated (1 times):
Field <com.effortguy.perftest.archunit.packagedependency.source.Source.foo> has type <com.effortguy.perftest.archunit.packagedependency.foo.Foo> in (Source.java:0)
2) 클래스 의존성 체크, 패키지 네이밍 룰 체크
시나리오
① Controller는 Service만 의존해야한다. Repsitory는 의존하면 안 된다. << 클래스 의존성 체크
② Controller는 Controller로 이름이 끝나야 한다. << 패키지 네이밍 룰 체크
테스트
아래와 같이 패키지와 클래스를 만들어줍니다.
TestController에만 소스 추가해줍니다.
public class TestController {
TestRepository testRepository;
}
결과
① Controller는 Service만 의존해야한다. Repsitory는 의존하면 안 된다. << Controller가 Repository를 의존하고 있으니 실패
② Controller는 Controller로 이름이 끝나야 한다. << 성공
package com.effortguy.perftest.archunit;
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 = "com.effortguy.perftest.archunit.classdependency")
public class ClassDependency {
@ArchTest // ①
ArchRule archRule = classes().that().haveNameMatching(".*Repository")
.should().onlyHaveDependentClassesThat().haveNameMatching(".*Service");
@ArchTest // ②
ArchRule archRule2 = classes().that().haveSimpleNameEndingWith("Controller")
.should().resideInAPackage("com.effortguy.perftest.archunit.classdependency.controller");
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have name matching '.*Repository' should only have dependent classes that have name matching '.*Service'' was violated (1 times):
Field <com.effortguy.perftest.archunit.classdependency.controller.TestController.testRepository> has type <com.effortguy.perftest.archunit.classdependency.repository.TestRepository> in (TestController.java:0)
3) 구현체 네이밍 룰 체크
시나리오
Connection 인터페스의 구현체들은 Connection으로 이름이 끝나야 한다.
테스트
아래와 같이 패키지, 클래스, 인터페이스를 만들겠습니다.
생성한 클래스 전부 Connection 을 구현하겠습니다.
// FtpConnection.java
public class FtpConnection implements Connection {
}
// HttpConnection.java
public class HttpConnection implements Connection {
}
// SshThing.java
public class SshThing implements Connection {
}
Connection 인터페스의 구현체들은 Connection으로 이름이 끝나야 한다. 테스트 코드는 다음과 같습니다.
@AnalyzeClasses(packages = "com.effortguy.perftest.archunit.inheritance")
public class Inheritance {
@ArchTest
ArchRule archRule = classes().that().implement(Connection.class)
.should().haveSimpleNameEndingWith("Connection");
}
결과
SshThing은 Connection의 구현체면서 이름이 Connection으로 끝나지 않기 때문에 테스트가 실패합니다.
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that implement com.effortguy.perftest.archunit.inheritance.Connection should have simple name ending with 'Connection'' was violated (1 times):
simple name of com.effortguy.perftest.archunit.inheritance.SshThing does not end with 'Connection' in (SshThing.java:0)
4) 구현체 어노테이션 체크
시나리오
Entity를 상속한 클래스는 @Transactional 어노테이션이 반드시 있어야 한다.
테스트
아래와 같이 패키지, 클래스를 만들겠습니다.
EntityManager를 상속하고 ValidPersistenceUser 클래스에만 @Transactional을 붙혀주겠습니다.
// illegalPersistenceUser.java
public class illegalPersistenceUser extends EntityManager {
}
// ValidPersistenceUser.javqa
@Transactional
public class ValidPersistenceUser extends EntityManager {
}
테스트 코드는 아래와 같습니다.
@AnalyzeClasses(packages = "com.effortguy.perftest.archunit.annotation")
public class Annotation {
@ArchTest
ArchRule archRule = classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat().areAnnotatedWith(Transactional.class);
}
결과
illegalPersistenceUser 클래스는 EntityManager를 상속받으면서 @Transactional 어노테이션이 없어 테스트 실패가 납니다.
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that implement com.effortguy.perftest.archunit.inheritance.Connection should have simple name ending with 'Connection'' was violated (1 times):
simple name of com.effortguy.perftest.archunit.inheritance.SshThing does not end with 'Connection' in (SshThing.java:0)
나머지 예제는 공식 유저 가이드를 참고 부탁드립니다.
간단하게 알아보고 추후에 사용하면 깊게 다시 정리하겠습니다.
'Java' 카테고리의 다른 글
[Java] Enum 사용법 (2) - 문법 (0) | 2022.09.06 |
---|---|
[Java] Enum 사용법 (1) - 탄생 배경 (0) | 2022.09.06 |
[Java] JUnit 5 사용법 (12) - AssertJ ( + vs Hamcrest, 마이그레이션) (1) | 2022.09.06 |
[Java] JUnit 5 사용법 (11) - Hamcrest (0) | 2022.09.06 |
[Java] JUnit 5 사용법 (10) - Assertions, Assumptions (0) | 2022.09.06 |
댓글