본문 바로가기
Java

[Java] 아키텍처 테스트 - ArchUnit (+ JUnit 5)

by 노력남자 2022. 9. 6.
반응형

이번 포스팅에선 아키텍처 테스트 라이브러리인 ArchUnit에 대해 알아보겠습니다.

 

아키텍처 테스트란?

 

아키텍처 테스트는 프로젝트가 정해놓은 아키텍처를 제대로 따르고 있는지 확인하는 것을 말합니다.

 

ArchUnit이란?

 

ArchUnit는 다양한 아키텍처 체크를 할 수 있는 메소드를 제공합니다.

 

1) 패키지 의존성 체크

2) 클래스 의존성 체크

3) 패키지 네이밍 룰 체크

4) 구현체 네이밍 룰 체크

5) 구현체 어노테이션 체크

6) 레이어 체크

7) 순환구조 체크

 

추가적인 내용은 공식 홈페이지 가이드를 참고하시길 바랍니다.

 

 

ArchUnit User Guide

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing giv

www.archunit.org

 

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)

 

나머지 예제는 공식 유저 가이드를 참고 부탁드립니다.

 

 

ArchUnit User Guide

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing giv

www.archunit.org

 

간단하게 알아보고 추후에 사용하면 깊게 다시 정리하겠습니다.

반응형

댓글