ArchUnitを入れて、プロジェクト内のアーキテクチャルールをテストする

エキサイト株式会社メディア事業部エンジニアの佐々木です。弊社アドベントカレンダー5日目を担当させていただきます。メディア事業部では、SpringBootを用いて日々ソフトウェア開発を行っていますが、サービスが大きくなったり人が増えたりするとアーキテクチャが障害にならないまでも部分的に守られていないことが起こります。Javaの世界ではArchUnitというアーキテクチャをテストしてくれるものがあり、弊社での利用を紹介いたします。

www.archunit.org

前提

openjdk 21.0.1 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-21.0.1.12.1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.1.12.1 (build 21.0.1+12-LTS, mixed mode, sharing)


------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.1 (Amazon.com Inc. 21.0.1+12-LTS)
OS:           Mac OS X 12.5 aarch64


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)

アプリケーションのサンプルコード

今回は、アプリケーションのアーキテクチャとしてサービス階層でトランザクションを貼るようなものだとします。要件としては、トランザクションを貼る際に接続するデータベースを決定します。デフォルトでは更新系DBを参照してますが、参照系はリードレプリDBに接続を期待します。そのアノテーション@Transactional(readonly=true)となります。

下記は、@Transactionalアノテーションを貼っていないケース、@Transactional(readOnly=false)アノテーションを貼っているケースで両方とも失敗することが期待値です。

@Service // @Transactionalのアノテーションを貼っていない
class DemoServiceImpl implements DemoService {

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    public Object get10SecondsExpirationData() throws InterruptedException {
        return "expiration 10 seconds \n";
    }

    @Override
    public Object get5SecondsExpirationData() throws InterruptedException {
        return "expiration 5 seconds \n";
    }
}

@Service
@Transactional(readOnly = false) // @Transactionalのアノテーションを貼っているのだが、readOnly=trueではない
class DemoService2Impl implements DemoService {

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    public Object get10SecondsExpirationData() throws InterruptedException {
        return "expiration 10 seconds \n";
    }

    @Override
    public Object get5SecondsExpirationData() throws InterruptedException {
        return "expiration 5 seconds \n";
    }
}

テストコード

class ArchitectureTest {
    @Test
    @DisplayName("@Serviceの場合には@Transactionalをつけるルール")
    void serviceAnnotationWithCacheableTest() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("jp.co.excite"); // テストをするパッケージを指定します。
        ArchRule archRule = classes()
                .that().areAnnotatedWith(Service.class)   // that() を使うと対象のクラスやメソッドを絞り込めます
                .should().beAnnotatedWith(Transactional.class);  // should()以降でアーキテクチャがどのような状態だったら正しいかを指定できます
        archRule.check(javaClasses); // アーキテクチャルールで抽出したクラスをテストします。

    }

    @Test
    @DisplayName("@Transactionalは、readonlyをつけるルール")
    void serviceAnnotationWithTransactionalHaveReadonlyProperty() {
        JavaClasses javaClasses = new ClassFileImporter().importPackages("jp.co.excite");  // テストをするパッケージを指定します。
        ArchRule archRule = classes()
                .that().areAnnotatedWith(Service.class)
                .and().areAnnotatedWith(Transactional.class)
                 
                // アーキテクチャルールが複雑な場合は、無名クラスでルールの作成できます。
                .should(new ArchCondition<>("@Serviceの場合には@Transactionalをつけて、中身はreadonly=trueにするルール") {
                    @Override
                    public void check(JavaClass item, ConditionEvents events) {
                        boolean valid = item.getAnnotationOfType(Transactional.class).readOnly();
                        if (!valid) {
                            events.add(SimpleConditionEvent.violated(item, item.getFullName()));
                        }
                    }
                });
        archRule.check(javaClasses);
    }

}

BDDのような書き方でアーキテクチャルールを書くことが可能です。

実行してみます。

ArchitectureTest > @Transactionalは、readonlyをつけるルール FAILED
    java.lang.AssertionError at ArchitectureTest.java:46

ArchitectureTest > @Serviceの場合には@Transactionalをつけるルール FAILED
    java.lang.AssertionError at ArchitectureTest.java:26

AdventcalendarApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:802
            Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException at DependencyDescriptor.java:218

@Transactionalは、readonlyをつけるルール FAILED @Serviceの場合には@Transactionalをつけるルール FAILED

期待通りテストは失敗しています。

まとめ

アーキテクチャをドキュメントに残したり、レビューで修正するのはとても大切なことですが、すり抜けてしまうことはあります。アプリケーションとしては動作してしまうので、見つけたときは修正にコストがかかってしまったりしますので、こういうテストが自動化できるのは大変ありがたいと思います。ドキュメント化をおろそかにすることなく、継続的にArchUnitの充実も指定校と思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com