Architecture Test Support
Apache Causeway provides a library of ArchUnit tests to verify the structure of your domain applications.
Maven Configuration
Dependency Management
If your application inherits from the Apache Causeway starter app (org.apache.causeway.app:causeway-app-starter-parent
) then that will define the version automatically:
<parent>
<groupId>org.apache.causeway.app</groupId>
<artifactId>causeway-app-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
Alternatively, import the core BOM. This is usually done in the top-level parent pom of your application:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.core</groupId>
<artifactId>causeway-core</artifactId>
<version>3.1.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
In addition, add an entry for the BOM of all the testing support libraries:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing</artifactId>
<scope>import</scope>
<type>pom</type>
<version>3.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Dependencies
In the webapp module(s) of your application, add the following dependency:
<dependencies>
<dependency>
<groupId>org.apache.causeway.testing</groupId>
<artifactId>causeway-testing-archtestsupport-applib</artifactId>
<scope>test</scope> (1)
</dependency>
</dependencies>
1 | this assumes that the architecture tests reside in src/test/java (but see notes on excluding tests, in the next section). |
Architecture_Test Boilerplate
It’s generally sufficient to have a single Architecture_Test
class that runs all architecture tests against all classes.
This should look something like:
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static org.apache.causeway.testing.archtestsupport.applib.domainrules.ArchitectureDomainRules.*;
@AnalyzeClasses(
packagesOf = {
CustomerModule.class (1)
, OrderModule.class (1)
, ProductModule.class (1)
},
importOptions = { ImportOption.DoNotIncludeTests.class } (2)
)
public class Architecture_Test { (3)
@ArchTest (4)
public static ArchRule every_DomainObject_must_specify_logicalTypeName = (5)
every_DomainObject_must_specify_logicalTypeName(); (6)
...
}
1 | the modules of the application to be scanned |
2 | excludes running against the tests themselves. But see caveat, below. |
3 | Recommended name.
Architecture tests run quickly, so it generally makes sense to name them as unit tests (with a Test suffix, to be picked up by surefire). |
4 | indicates this is an architecture test.
There can be many such tests in a single file; all are expressed as static fields. |
5 | the field name is unimportant (but must be unique, of course). |
6 | the architecture test itself, imported from ArchitectureXxxRules |
Don’t architecture check your tests
In theory tests can be skipped from checking using the If this does not work for you, consider creating a separate Maven module as a peer of your webapp, and place the architecture tests in |
Domain rules
Checks for rules defined against domain classes reside in the ArchitectureDomainRules
library.
-
Strongly Recommended
-
logical type name
This ensures that persisted/serialized bookmarks of domain objects are stable even if the domain class' physical canonical name changes over time due to refactoring etc.:
-
every_DomainObject_must_specify_logicalTypeName()
-
every_DomainService_must_specify_logicalTypeName()
-
-
unique logical type name
This ensures that the logical type name is also unique across
@DomainObject
s and@DomainService
s.Note: this rule ignores any abstract classes because strictly the framework only requires that instantiatable classes have unique logical type names.
-
JAXB view models are annotated with
@DomainObject(nature=VIEW_MODEL)
This ensures that the framework is able to correctly detect the view models in the metamodel:
-
every_jaxb_view_model_must_also_be_annotated_with_DomainObject_nature_VIEWMODEL()
-
-
allow injected fields in view models (strongly recommended)
Ensures that view models safely ignore injected services.
-
every_injected_field_of_jaxb_view_model_must_be_annotated_with_XmlTransient()
-
every_injected_field_of_serializable_view_model_must_be_transient()
-
-
-
Recommended
-
consistency with repository finders
Requires that the caller of a repository finder handle the case that there may be no such object in the database:
-
every_finder_method_in_Repository_must_return_either_Collection_or_Optional()
-
-
mixin naming and coding conventions
For teams that rely on the
Mixee_memberName
convention to find mixins of domain objects:-
every_Action_mixin_must_follow_naming_convention()
-
every_Property_mixin_must_follow_naming_convention()
-
every_Collection_mixin_must_follow_naming_convention()
-
-
-
Optional
-
layout annotations
For teams that prefer to use annotations instead of layout files:
-
every_DomainObject_must_also_be_annotated_with_DomainObjectLayout()
-
every_DomainService_must_also_be_annotated_with_DomainServiceLayout()
-
-
repository class naming/annotation conventions (optional)
For teams that prefer that repositories are easily identifiable by their name
XxxRepository
, (and are annotated accordingly):-
every_Repository_must_follow_naming_conventions()
-
every_class_named_Repository_must_be_annotated_correctly()
-
-
controller class naming/annotation conventions (optional)
For teams that prefer that custom controllers are easily identifiable by their name
XxxController
, (and are annotated accordingly):-
every_Controller_must_be_follow_naming_conventions()
-
every_class_named_Controller_must_be_annotated_correctly()
-
-
None of these rules take any arguments, so simply include (as a static field) those that you wish to enforce.
Entity Checks
Entity rule checks are provided for both JPA/Eclipselink and JDO/DataNucleus, in ArchitectureJpaRules
and ArchitectureJdoRules
respectively:
-
Strongly Recommended
-
ensure JPA entities can support auditing and dependency injection:
Required in almost all cases:
-
every_jpa_Entity_must_be_annotated_as_an_CausewayEntityListener()
-
-
ensure entities can be referenced in JAXB view models:
Required to allow JAXB view models to transparently reference entities:
-
every_jpa_Entity_must_be_annotated_with_XmlJavaAdapter_of_PersistentEntityAdapter()
-
every_jdo_PersistenceCapable_must_be_annotated_as_XmlJavaAdapter_PersistentEntityAdapter()
-
-
ensure JPA entity defines a surrogate key:
Enforces a very common convention for JPA entities:
-
every_jpa_Entity_must_have_an_id_field()
-
-
ensure JDO entity’s logicalTypeName matches its discriminator (if any)
Both the logical type name and the
@Discriminator
for subclasses are a unique identifier of a type; this rule says they should be the same value.-
every_logicalTypeName_and_jdo_discriminator_must_be_same()
-
-
-
Recommended
-
encourage entities be organised into schemas:
for teams adopting a "(micro) service-oriented" mindset:
-
every_jpa_Entity_must_be_annotated_as_Table_with_schema()
-
every_jdo_PersistenceCapable_must_have_schema()
-
-
ensure JPA enum fields are persisted as strings:
For teams that prefer to query the database with enum values persisted as strings rather than as ordinal numbers:
-
every_enum_field_of_jpa_Entity_must_be_annotated_with_Enumerable_STRING()
-
-
ensure entities can be used in
SortedSet
parented collections:For teams that like to use
SortedSet
for parented collections, and rely on the entity to render itself in a "sensible" order:-
every_jpa_Entity_must_implement_Comparable()
-
every_jdo_PersistenceCapable_must_implement_Comparable()
-
-
ensure entities have a unique business key:
For teams that insist on business keys in addition to surrogate keys
-
every_jpa_Entity_must_be_annotated_as_Table_with_uniqueConstraints()
-
every_jdo_PersistenceCapable_must_be_annotated_as_Uniques_or_Unique()
-
-
-
Optional
-
ensure conformance of persistence vs Causeway annotations
For teams that like to emphasise entities vs view models:
-
every_jpa_Entity_must_be_annotated_with_DomainObject_nature_of_ENTITY()
-
every_jdo_PersistenceCapable_must_be_annotated_with_DomainObject_nature_of_ENTITY
-
-
ensure if JDO entity has a surrogate key then its annotations are consistent:
For teams that like to be explicit about the use of surrogate keys:
-
every_jdo_PersistenceCapable_with_DATASTORE_identityType_must_be_annotated_as_DataStoreIdentity()
-
-
enable injected fields in entities:
For teams that use type-safe queries (and want to ensure that services do not appear superfluously as fields in the "QXxx" classes):
-
every_injected_field_of_jpa_Entity_must_be_annotated_with_Transient()
-
every_injected_field_of_jdo_PersistenceCapable_must_be_annotated_with_NotPersistent()
-
-
encourage optimistic locking:
For teams that prefer optimistic locking as a standard everywhere:
-
every_jpa_Entity_must_have_a_version_field()
-
every_jdo_PersistenceCapable_must_be_annotated_with_Version()
-
-
encourage static factory methods for entities:
For teams that prefer the use of factory methods to vanilla constructors:
-
every_jpa_Entity_must_have_protected_no_arg_constructor()
-
-
None of these rules takes any arguments, so simply call those (from a static field) those that you wish to enforce.
Module Rules
The module rules, residing in ArchitectureModuleRules
class, are used to check the coarse-grained dependencies between modules [1], to ensure proper layering.
A further check checks at a more fine-grained level for the placement of classes into subpackages of those modules.
code_dependencies_follow_module_Imports
The @Import
statements between modules form an acyclic dependency graph.
If there is a cycle between those modules, then Spring itself will fail to boot.
This architecture check doesn’t check the the modules' graph itself, but rather ensures that the dependencies between classes that live within each module honour the direction of the module graph.
For example, if the OrderModule
depends on (@Import
s) the CustomerModule
, then we allow for the Order
entity to reference the Customer
entity.
However, we do not allow the opposite reference.
To run the test, use:
@ArchTest
public static ArchRule code_dependencies_follow_module_Imports =
code_dependencies_follow_module_Imports( (1)
analyzeClasses_packagesOf(Architecture_Test.class)); (2)
}
1 | architecture test itself, imported from ArchitectureModuleRules |
2 | utility that passes in for all analysis all of the module classes that are annotated on Architecture_Test . |
Aside
You might well ask: if deleting a customer requires deleting its orders, then how is this done if a customer does not "know" abouts its related orders?
There are two main options:
-
use a mixin. Provide a
Customer_delete
action, but place it in theOrderModule
. That way the mixin code has access to delete the associatedOrder
sHowever, this trick won’t work if there are multiple referencing modules to customer, all of which need to do their work but none of which know about the other. For example, if there is also an
AddressModule
that maintains the customer’s address, and neitherOrderModule
norAddressModule
know about the other, then there is no one place to put the mixin that has knowledge of all of the related "stuff" of customer to be dealt with. -
use a domain event.
Here we keep the
Customer_delete
mixin in theCustomerModule
(indeed, it could even be a regular action) and instead it emits a domain event:Customer_delete.java@Action(domainEvent = Customer_delete.ActionEvent.class) @RequiredArgsConstructor public class Customer_delete { public static class ActionEvent (1) extends ActionDomainEvent<Customer_delete>{} private final Customer customer; public void act() { customerRepo.delete(customer); } }
1 event to be fired.
Module Package Tests
The module package tests builds upon the module layering tests and also checks that that each class is in the appropriate subpackage within each module, and that those subpackages access only the allowed subpackages of its own module or of other modules. Different sets of subpackages can be defined for both the "local" (same) module and "foreign" (referencing) modules.
Because different teams will undoubtedly have different standards, the exact rules for subpackages are pluggable, with the Subpackage
interface defining the information that the architecture test requires:
public interface Subpackage {
String getName(); (1)
List<String> mayBeAccessedBySubpackagesInSameModule(); (2)
List<String> mayBeAccessedBySubpackagesInReferencingModules(); (3)
default String packageIdentifier() { (4)
return "." + getName() + "..";
}
}
1 | The name of the subpackage, for example "dom", "api", "spi" or "fixtures". |
2 | A list of the (names of the) subpackages where classes in the same module as this package have access.
For example, the "dom" subpackage can probably be referenced from the "menu" subpackage, but not vice versa. The special value of "*" is a wildcard meaning that all subpackages (in the same module) can access. |
3 | A list of the (names of the) subpackages where classes in the packages of other referencing modules may have access.
For example, in some cases the the "dom" subpackage may <i>not</i> be accessible from other modules if the intention is to require all programmatic access through an "api" subpackage (where the classes in The special value of "*" is a wildcard meaning that all subpackages (in other modules) can access. |
4 | allows a subpackage to more precisely control the classes that it includes.
Useful if want to declare a subpackage representing that of the module’s own package (where the XxxModule class resides). |
The SubpackageEnum
provides an off the shelf implementation; you will probably want to copy this and adjust as necessary.
To run the test, use:
@ArchTest
public static ArchRule code_deps_follow_module_Imports_and_subpackage_rules =
code_dependencies_follow_module_Imports_and_subpackage_rules( (1)
analyzeClasses_packagesOf(ModulesArchTests.class), (2)
asList(SimplifiedSubpackageEnum.values())); (3)
1 | architecture test itself, imported from ArchitectureModuleRules |
2 | utility that passes in for all analysis all of the module classes that are annotated on Architecture_Test . |
3 | list of the Subpackage s whose relationships are to be checked |
where for example Subpackage
is implemented using with:
@RequiredArgsConstructor
public enum SimplifiedSubpackageEnum implements Subpackage {
dom(
singletonList("*"), (1)
emptyList() (2)
),
api(
singletonList("*"), (1)
singletonList("*") (3)
),
spi(
singletonList("dom"), (4)
singletonList("spiimpl") (5)
),
spiimpl(
emptyList(), (6)
emptyList() (2)
),
;
final List<String> local;
final List<String> referencing;
@Override
public String getName() { return name(); }
@Override
public List<String> mayBeAccessedBySubpackagesInSameModule() {
return local;
}
@Override
public List<String> mayBeAccessedBySubpackagesInReferencingModules() {
return referencing;
}
private static String[] asArray(List<String> list) {
return list != null ? list.toArray(new String[] {}) : null;
}
}
1 | wildcard means that all subpackages in this module can access this module |
2 | no direct access from other modules |
3 | wildcard means that all subpackages in other modules can access this module |
4 | callers of a module’s own SPI |
5 | other modules should only implement the SPI |
6 | no direct access from this module |
@Configuration