Code review manual sering gagal menangkap pelanggaran struktural yang halus. Pengembang baru dapat memanggil repository langsung dari controller, membuat siklus dependensi (circular dependency) antar-package, atau melewatkan konvensi penamaan standar. Seiring waktu, batas arsitektur menipis dan sistem terdegradasi menjadi big ball of mud.
ArchUnit menyelesaikan masalah ini dengan memperlakukan arsitektur sebagai kode. ArchUnit adalah pustaka pengujian Java yang menganalisis bytecode file .class untuk memastikan aturan arsitektur dipatuhi melalui unit test standar. Artikel ini menguraikan konfigurasi ArchUnit pada Spring Boot, pembuatan rules penting, strategi mitigasi legacy code, dan integrasi pipeline CI.
Setup Dependency
ArchUnit berjalan di atas engine JUnit 5 tanpa memerlukan runtime Spring context aktif, sehingga eksekusi rule berlangsung dalam hitungan detik.
Untuk Maven, tambahkan dependency ke pom.xml:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>Untuk Gradle (Kotlin DSL), tambahkan ke build.gradle.kts:
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")Menulis Rule Arsitektur
ArchUnit menyediakan Fluent API untuk mendefinisikan batasan kode. Buat class pengujian baru di dalam direktori src/test/java.
1. Penegakan Layer Arsitektur
Pola baku Spring Boot umumnya memisahkan Controller, Service, dan Repository. Controller tidak boleh mengakses Repository secara langsung, dan Repository tidak boleh memanggil Service.
package com.example.app.architecture;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.core.importer.ImportOption;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@AnalyzeClasses(packages = "com.example.app", importOptions = ImportOption.DoNotIncludeTests.class)
public class LayerArchitectureTest {
@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller", "Service")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
}2. Larangan Circular Dependency Antar-Paket
Circular dependency menyebabkan kopling ketat yang menyulitkan modularisasi dan testing. Library Slices pada ArchUnit mendeteksi dependensi melingkar di tingkat sub-package.
package com.example.app.architecture;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.core.importer.ImportOption;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
@AnalyzeClasses(packages = "com.example.app", importOptions = ImportOption.DoNotIncludeTests.class)
public class CircularDependencyTest {
@ArchTest
static final ArchRule no_cycles_between_feature_slices = slices()
.matching("com.example.app.(*)..")
.should().beFreeOfCycles();
}3. Konvensi Penamaan dan Anotasi Spring
Untuk menjaga konsistensi, pastikan class yang diberi anotasi Spring mengikuti standar penamaan yang seragam.
package com.example.app.architecture;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.core.importer.ImportOption;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RestController;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
@AnalyzeClasses(packages = "com.example.app", importOptions = ImportOption.DoNotIncludeTests.class)
public class NamingConventionTest {
@ArchTest
static final ArchRule controllers_must_be_annotated_and_named = classes()
.that().resideInAPackage("..controller..")
.should().beAnnotatedWith(RestController.class)
.andShould().haveSimpleNameEndingWith("Controller");
@ArchTest
static final ArchRule services_must_be_annotated_with_service = classes()
.that().haveSimpleNameEndingWith("Service")
.should().beAnnotatedWith(Service.class);
}Strategi Baseline untuk Legacy Code
Menerapkan ArchUnit pada sistem yang sudah berjalan sering menghasilkan ratusan kegagalan pengujian sekaligus. Menghentikan pengembangan fitur demi memperbaiki seluruh pelanggaran lama bukan langkah realistis. Gunakan fitur FreezingArchRule untuk membuat baseline pelanggaran.
Aktifkan baseline storage dengan membuat file src/test/resources/archunit.properties:
freeze.store.default.allowStoreUpdate=true
freeze.store.default.path=src/test/resources/archunit_storeBungkus rule yang memiliki utang teknis dengan FreezingArchRule.freeze():
import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze;
@ArchTest
static final ArchRule layer_dependencies_are_respected = freeze(
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller", "Service")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service")
);Mekanisme kerja Freezing:
- Saat dijalankan pertama kali, ArchUnit merekam seluruh pelanggaran yang ada ke dalam folder
archunit_storedan meluluskan build. - Ubah properti
freeze.store.default.allowStoreUpdate=falsepada environment CI. - Jika ada developer yang menambahkan pelanggaran baru, test akan gagal dan memblokir pipeline.
- Jika pelanggaran lama diperbaiki secara bertahap, perbarui store secara berkala.
Integrasi ke Pipeline CI (GitHub Actions)
Karena ArchUnit berjalan di atas JUnit 5, eksekusi dilakukan bersamaan dengan unit test tanpa konfigurasi plugin tambahan. Buat workflow GitHub Actions di .github/workflows/ci.yml:
name: CI Pipeline
on:
pull_request:
branches: [ main, master ]
jobs:
architecture-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Run ArchUnit Tests
run: mvn test -Dtest=*ArchitectureTest,*TestJika ada rule yang dilanggar, ArchUnit mencetak pesan error spesifik beserta nama class, method, dan nomor baris penyebab error, lalu mengembalikan status exit code non-zero yang membatalkan merge pull request.
Troubleshooting dan Optimasi
- Waktu Scan Lambat: Scope scan yang terlalu luas (misal menyertakan class pihak ketiga) memperlambat evaluasi bytecode. Batasi target package secara eksplisit pada anotasi
@AnalyzeClasses(packages = "com.example.app"). - Spring Context Overhead: Jangan gunakan
@SpringBootTestpada test suite ArchUnit. ArchUnit hanya membutuhkan Java reflection dan ASM bytecode analyzer. Memuat ApplicationContext Spring akan memperlambat build tanpa memberikan nilai tambah. - Sub-package Naming: Penggunaan
..package..mencakup package tersebut beserta seluruh turunannya, sedangkan.package.hanya mencakup layer tepat di bawahnya. Ketelitian penulisan syntax regex krusial agar pengujian tidak meloloskan pelanggaran.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!