1

I have a demo project which tries to respect strictly clean/onion/hexagonal architecture. Here is how I configure ArchUnit test :

    @AnalyzeClasses(packages ="fr.tristan.demoassurance", importOptions = {ImportOption.DoNotIncludeTests.class, ExcludeConfig.class})
public class ArchitectureTest {

    @ArchTest
    public static final ArchRule onionArchitectureRule = onionArchitecture()
        .domainModels("fr.tristan.demoassurance.domain.entity..")
        .domainModels("fr.tristan.demoassurance.domain.event..")
        .domainServices("fr.tristan.demoassurance.domain.api..")
        .applicationServices("fr.tristan.demoassurance.application.controller")
        .adapter("message", "fr.tristan.demoassurance.infrastructure.message..")
        .adapter("persistence-mongodb", "fr.tristan.demoassurance.infrastructure.repository.mongodb..")
        .adapter("persistence-mysql", "fr.tristan.demoassurance.infrastructure.repository.mysql..")
        ;
}

ArchUnit complains about one parameter :

was violated (1 times):
Method <fr.tristan.demoassurance.domain.spi.message.MessageQueueSpi.send(fr.tristan.demoassurance.domain.event.PoliceAssuranceEvent)> has parameter of type <fr.tristan.demoassurance.domain.event.PoliceAssuranceEvent> in (MessageQueueSpi.java:0)

Here is the "guilty" method :

package fr.tristan.demoassurance.domain.spi.message;

import fr.tristan.demoassurance.domain.event.PoliceAssuranceEvent;

public interface MessageQueueSpi {
    void send(PoliceAssuranceEvent policeAssuranceCreatedEvent);
}

This makes no sense for me because :

  1. I don't see which rule is violated, it's just not explained why a SPI interface should not have a parameter of a domain model type.

  2. I have an other interface in the same fr.tristan.demoassurance.domain.spi parent package with a savemethod which also have a parameter PoliceAssurance from "domain models" and ArchUnit doesn't complain about it :

package fr.tristan.demoassurance.domain.spi.repository;

import java.util.List;

import fr.tristan.demoassurance.domain.entity.PoliceAssurance;
import fr.tristan.demoassurance.domain.exception.PoliceAssuranceException;

public interface PoliceAssuranceRepositorySpi {

    List<PoliceAssurance> findAll();
    PoliceAssurance save(PoliceAssurance policeAssurance) throws PoliceAssuranceException;
    PoliceAssurance findById(Integer id) throws PoliceAssuranceException;
    void deleteById(Integer id) throws PoliceAssuranceException;    
}
Tristan
  • 8,733
  • 7
  • 48
  • 96

2 Answers2

1

As you found out, domainModels behaves like a setter, i.e. does not extend the internal domainModelPredicate (and likewiese for domainServices and applicationServices). That is, when calling the methods multiple times, previous package identifiers are simply overwritten.

Manfred
  • 2,269
  • 1
  • 5
  • 14
  • Would it have helped you if the second invocation had logged a warning? We could also discuss whether it should even throw an exception, but that might break intentional usages. – Manfred May 15 '23 at 19:26
  • Yes it was one of the problem. The other problem is more concerning : you force me to declare "service provider interface" (spi) as "domain services" or "adapter" where it should have its own category imo. Because of this, in your "samples", the "domain" package depends on the "persistence" package (https://github.com/TNG/ArchUnit-Examples/blob/main/example-junit5/src/main/java/com/tngtech/archunit/example/onionarchitecture/domain/service/ShoppingService.java depends on "com.tngtech.archunit.example.onionarchitecture.adapter.persistence") which is layered architecture but not onion/clean/hexa – Tristan May 16 '23 at 05:47
  • The discussion about how to improve ArchUnit will be continued on https://github.com/TNG/ArchUnit/issues/1111. – Manfred May 20 '23 at 10:41
0

I made it work by :

  1. putting domain models in the same "domainModels(...)"
  2. adding mappers and spi interfaces to respectively "applicationServices(...)" and "domainServices(...)"
@AnalyzeClasses(packages ="fr.tristan.demoassurance", importOptions = {ImportOption.DoNotIncludeTests.class, ExcludeConfig.class, ExcludeTestFactory.class})
public class ArchitectureTest {


    @ArchTest
    public static final ArchRule onionArchitectureRule = onionArchitecture()
        .domainModels("fr.tristan.demoassurance.domain.entity..", "fr.tristan.demoassurance.domain.event..")
        .domainServices("fr.tristan.demoassurance.domain.api..", "fr.tristan.demoassurance.domain.spi..")
        .applicationServices("fr.tristan.demoassurance.application.controller..", "fr.tristan.demoassurance.application.mapper..")
        .adapter("message", "fr.tristan.demoassurance.infrastructure.message..")
        .adapter("persistence-mongodb", "fr.tristan.demoassurance.infrastructure.repository.mongodb..")
        .adapter("persistence-mysql", "fr.tristan.demoassurance.infrastructure.repository.mysql..")
        ;
}
Tristan
  • 8,733
  • 7
  • 48
  • 96