تنفيذ توصيات هيكل الكود باستخدام ArchUnit

عند إنشاء البرامج ، نتفق جميعًا ، كفريق واحد ، على اتباع مجموعة من الإرشادات التي تعتبر بشكل عام أفضل الممارسات. ومع ذلك ، أثناء التطوير ، قد يخالف المطورون هذه القواعد عن غير قصد أو عن غير قصد. عادة ما نعتمد على  مراجعات الكود  أو أدوات فحص جودة الكود مثل  SonarQube  و  PMD  وما إلى ذلك. للتحقق من وجود مثل هذه الانتهاكات. ولكن قد تكون بعض التوصيات حلولًا لا يمكن أتمتتها باستخدام SonarQube و PMD وما إلى ذلك.

على سبيل المثال ، عادةً ما أرغب في اتباع الإرشادات أدناه لتطبيقاتي القائمة على Java:

  1. اتبع هيكلًا من ثلاث طبقات  (الويب ، الخدمة ، طبقات المستودع) حيث يمكن لأي طبقة أن تتفاعل فقط مع الطبقة الدنيا المباشرة ، ويجب ألا تتفاعل الطبقة الدنيا مع الطبقة العليا. أولئك. يمكن لطبقة الويب التفاعل مع طبقة الخدمة ، ويمكن لطبقة الخدمة أن تتفاعل مع طبقة المستودع. لكن طبقة المستودع لا يمكنها الاتصال بالخدمة أو طبقة الويب ، ولا يمكن لطبقة الخدمة التفاعل مع طبقة الويب.

  2. إذا كان التطبيق كبيرًا ، فقد نرغب في اتباع بنية Package-By-Feature ، حيث تكون مكونات الويب والخدمة فقط عامة ويجب أن تكون بقية المكونات خاصة بالحزمة.

  3. عند استخدام Spring Dependency Injection ، لا تستخدم الحقن الميداني وتفضل الحقن المستند إلى المنشئ.

وبالتالي ، قد يكون هناك العديد من القواعد التي نريد اتباعها. الخبر السار هو أنه يمكننا التحقق من تنفيذ هذه التوصيات باستخدام اختبارات JUnit باستخدام ArchUnit .

هنا دليل مستخدم ArchUnit .

دعونا نرى كيف يمكننا استخدام ArchUnit لاختبار إرشادات البنية الخاصة بنا.

أضف التبعية التالية archunit-junit5 .

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.13.1</version>
    <scope>test</scope>
</dependency>

دعنا نلقي نظرة على كيفية تطبيق الإرشادات المختلفة التي ذكرتها أعلاه.

القاعدة 1. يجب ألا تتفاعل الخدمات والمستودعات مع طبقة الويب.

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 الرسمي   ومعرفة الأشياء الرائعة التي يمكنك القيام بها باستخدام  ArchUnit .




All Articles