在构建软件时,我们所有人作为一个团队,都同意遵循通常被认为是最佳实践的一组准则。但是,在开发过程中,开发人员可能在不知不觉中或不愿意违反这些规则。我们通常依靠 代码审查 或代码质量检查器(如 SonarQube , PMD 等)。检查是否存在此类违规行为。但是其中一些建议可能是无法使用SonarQube,PMD等自动化的解决方案。
例如,对于基于Java的应用程序,我通常希望遵循以下准则:
遵循三层结构 (Web,服务,存储库层),其中任何层都只能与直接下层交互,而下层则不应与上层交互。那些。Web层可以与服务层交互,服务层可以与存储库层交互。但是存储库层无法与服务或Web层进行通信,服务层无法与Web层进行交互。
如果应用程序很大,我们可能希望遵循Package-By-Feature结构,其中只有Web和Service组件是公共的,其余组件必须是Package-private的。
使用Spring Dependency Injection时,不要使用基于字段的注入,而应该使用基于构造函数的注入。
因此,我们可能要遵循许多规则。好消息是,我们可以使用ArchUnit使用JUnit测试来验证这些建议的实现。
这里指导用户ArchUnit。
让我们看看如何使用ArchUnit来测试我们的架构准则。
添加以下依赖项archunit-junit5。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
让我们看一下如何应用上面提到的各种准则。
规则1.服务和存储库不应与Web层交互。
package com.sivalabs.moviebuffs;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
class ArchTest {
@Test
void servicesAndRepositoriesShouldNotDependOnWebLayer() {
JavaClasses importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.sivalabs.moviebuffs");
noClasses()
.that().resideInAnyPackage("com.sivalabs.moviebuffs.core.service..")
.or().resideInAnyPackage("com.sivalabs.moviebuffs.core.repository..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("com.sivalabs.moviebuffs.web..")
.because("Services and repositories should not depend on web layer")
.check(importedClasses);
}
}
ArchUnit DSL, , . , , .
2:
SpringBoot , . , Web Config .
.
@Test
void shouldFollowLayeredArchitecture() {
JavaClasses importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.sivalabs.moviebuffs");
layeredArchitecture()
.layer("Web").definedBy("..web..")
.layer("Config").definedBy("..config..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..repository..")
.whereLayer("Web").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Config", "Web")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
.check(importedClasses);
}
3: Spring @Autowired
@Test
void shouldNotUseFieldInjection() {
JavaClasses importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.sivalabs.moviebuffs");
noFields()
.should().beAnnotatedWith(Autowired.class)
.check(importedClasses);
}
4:
, , Service ..
@Test
void shouldFollowNamingConvention() {
JavaClasses importedClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.sivalabs.moviebuffs");
classes()
.that().resideInAPackage("com.sivalabs.moviebuffs.core.repository")
.should().haveSimpleNameEndingWith("Repository")
.check(importedClasses);
classes()
.that().resideInAPackage("com.sivalabs.moviebuffs.core.service")
.should().haveSimpleNameEndingWith("Service")
.check(importedClasses);
}
5: JUnit 5
JUnit 5 . JUnit 4 (… Testcontainers … ), / JUnit4 , @Test , Assert .. .
JUnit 4 :
@Test
void shouldNotUseJunit4Classes() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.sivalabs.moviebuffs");
noClasses()
.should().accessClassesThat().resideInAnyPackage("org.junit")
.because("Tests should use Junit5 instead of Junit4")
.check(classes);
noMethods().should().beAnnotatedWith("org.junit.Test")
.orShould().beAnnotatedWith("org.junit.Ignore")
.because("Tests should use Junit5 instead of Junit4")
.check(classes);
}
, .
请阅读官方的 ArchUnit UserGuide ,了解使用ArchUnit可以做些 什么。