// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* Spring Boot 2.4.0
* JUnit 5.7.0
* ArchUnit 0.14.1
アーキテクチャテストのモチベーション
アプリケーション固有の重要な実装ルールを担保したい。
- バックエンドの REST API サービスを Spring Boot で構築している
- API エンドポイントとなるハンドラーメソッドでは HandlerInterceptor と自作アノテーション(
@Auth
)によって、利用者の「認証」と「認可(権限チェック)」を行っている - 認証・認可のアノテーションを付与し忘れると、エンドポイントがパブリックに公開されてしまうため、付与忘れがないことを漏れなくチェックしたい
ハンドラーメソッドの実装イメージ
// ブログの新規投稿エンドポイント
// `EDITOR`権限以上のユーザのみエンドポイントを利用できる
@Auth(Role.EDITOR)
@PostMapping("/api/blogs")
public Map<String, Object> postBlog(@RequestBody final BlogCreateForm form) {
// ...
}
アーキテクチャテストの実装
package com.example;
import com.example.presentation.Auth;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.*;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
class ArchitectureTest {
// 検査対象のクラス
private static final JavaClasses CLASSES =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.example");
@Test
void APIエンドポイントでは必ず認証と認可を行う() {
methods()
.that(new DescribedPredicate<>("are request handler") {
/**
* @param method 検査対象クラスのメソッド
* @return ハンドラーメソッドである場合、true
*/
@Override
public boolean apply(JavaMethod method) {
return method.isAnnotatedWith(RequestMapping.class)
|| method.isAnnotatedWith(GetMapping.class)
|| method.isAnnotatedWith(PostMapping.class)
|| method.isAnnotatedWith(PutMapping.class)
|| method.isAnnotatedWith(PatchMapping.class)
|| method.isAnnotatedWith(DeleteMapping.class);
}
})
.should(new ArchCondition<>("be annotated with @Auth") {
@Override
public void check(JavaMethod method, ConditionEvents events) {
if (! method.isAnnotatedWith(Auth.class)) {
// 実装ルール違反を通知
events.add(
SimpleConditionEvent.violated(method, String.format(
"`%s` is not annotated with @Auth.", method.getFullName()))
);
}
}
})
.check(CLASSES);
}
}