Implementing architectural fitness functions using Gradle, JUnit and code-assert
Architectural fitness functions
Inspired by Neal Ford’s presentation at our Change is the Only constant event I started experimenting with architectural fitness functions. An architectural fitness function provides an objective integrity assessment of some architectural characteristic(s). If you want to take a deeper dive into evolutionary architectures including fitness functions take look at Neals book: Building Evolutionary Architectures: Support Constant Change. Neal’s slides contained an example of verifying package dependencies from a Unit Test using JDepend.
Verifying code modularity
In this blog post we’ll elaborate on that approach and create a Unit Test that verifies that our code complies to the chosen packaging strategies using an alternative to JDepend named code-assert. We’ll verify two types of packaging strategies; package by layer and package by feature. For a definition of these strategies please have a look at this blog from Simon Brown.
Creating the fitness functions
To illustrate both types and their fitness functions I created a small demo project that contains a package structure for each of this strategies containing dummy classes/components. Needless to say that this is for demonstrating purposes only and normally you would choose only one packaging strategy for a project and stick to that, preferable package by feature ;)
Adding code-assert to our Gradle project
Lets start by adding the dependency to code-assert to our Gradle build file dependencies.
dependencies {
compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.6.1'
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile group: 'guru.nidi', name: 'code-assert', version: '0.8.2'
}
Configuring code-assert for Gradle
Update: as of version 0.8.4 code-asserts now supports Gradle with AnalyzerConfig.gradle() directly. The first step in each Unit Test is to configure code-assert and specify where the sources and classes resist on our filesystem. For Maven code-assert includes an easy way to create this configuration but it lacks this feature for Gradle (for now). I’ve created a GradleAnalyzerConfig class which you can use to create the correct configuration. A pull request to the code-assert project is on it’s way.
Verifying package by layer
Now we can create a Unit Test that will verify if our code is packaged by layer.
public class VerifyPackageByLayerTest {
@Test
public void verifyPackageByLayer() {
/// Create an analyzer config for the package we'd like to verify
AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main("com.jdriven.fitness.packaging.by.layer");
// Dependency rules for Packaging by Layer
// NOTE: the classname should match the packagename
class ComJdrivenFitnessPackagingByLayer extends DependencyRuler {
// Rules for layer child packages
// NOTE: they should match the name of the sub packages
DependencyRule controller, service, repository;
@Override
public void defineRules() {
// Our App classes depends on all subpackages because it constructs all of them
base().mayUse(base().allSub());
// Controllers may use Services
controller.mayUse(service);
// Services may use Repositories
service.mayUse(repository);
}
}
// All dependencies are forbidden, except the ones defined in ComJdrivenFitnessPackagingByLayer
// java, org, net packages may be used freely
DependencyRules rules = DependencyRules.denyAll()
.withRelativeRules(new ComJdrivenFitnessPackagingByLayer())
.withExternals("java.*", "org.*", "net.*");
DependencyResult result = new DependencyAnalyzer(analyzerConfig).rules(rules).analyze();
assertThat(result, matchesRulesExactly());
}
}
If we execute this Unit Test it will pass because our demo code complies with the specified rules. Now we modify the code by accessing a Repository directly from our Controller and change App.java accordingly.
public class ControllerA {
private final ServiceA serviceA;
private final RepositoryA repositoryA;
public ControllerA(ServiceA serviceA, RepositoryA repositoryA) {
this.serviceA = serviceA;
this.repositoryA = repositoryA;
}
}
Execute the Unit Test again and it will fail, because we violated our rules by accessing the Repository directly from our Controller:
java.lang.AssertionError:
Expected: Comply with rules
but: DENIED com.jdriven.fitness.packaging.by.layer.controller ->
com.jdriven.fitness.packaging.by.layer.repository (by com.jdriven.fitness.packaging.by.layer.controller.ControllerA)
Verifying Package By Feature
The following Unit Test will verify our package by feature strategy:
public class VerifyPackageByFeatureTest {
@Test
public void verifyPackageByFeature() {
/// Create an analyzer config for the package we'd like to verify
AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main("com.jdriven.fitness.packaging.by.feature");
// Dependency Rules for Packaging By Feature
// NOTE: the classname should match the packagename
class ComJdrivenFitnessPackagingByFeature extends DependencyRuler {
// Rules for feature child packages
// NOTE: they should match the name of the sub packages
DependencyRule a, b;
@Override
public void defineRules() {
// Our App classes depends on all subpackages because it constructs all of them
base().mayUse(base().allSub());
}
}
// All dependencies are forbidden, except the ones defined in ComJdrivenFitnessPackagingByFeature
// java, org, net packages may be used freely
DependencyRules rules = DependencyRules.denyAll()
.withRelativeRules(new ComJdrivenFitnessPackagingByFeature())
.withExternals("java.*", "org.*", "net.*");
DependencyResult result = new DependencyAnalyzer(analyzerConfig).rules(rules).analyze();
assertThat(result, matchesRulesExactly());
}
}
The Unit Test is almost identical to the previous one, the rules are even less complicated. We allow our App class to access all, that’s it. There are no dependencies allowed between packages a and b. But what if we need to use ServiceB from ControllerA?
public class ControllerA {
private final ServiceA serviceA;
private final ServiceB serviceB;
public ControllerA(ServiceA serviceA, ServiceB serviceB) {
this.serviceA = serviceA;
this.serviceB = serviceB;
}
}
Initially our test will fail:
java.lang.AssertionError:
Expected: Comply with rules
but: DENIED com.jdriven.fitness.packaging.by.feature.a ->
com.jdriven.fitness.packaging.by.feature.b (by com.jdriven.fitness.packaging.by.feature.a.ControllerA)
But if the dependency is really needed and we really thought this through we make this explicit by changing the Unit Test and adding the following rule:
// Allow package / module a to acces b
a.mayUse(b);
Bonus: a cyclic dependency test
Adding the inter package dependencies might lead to a cyclic dependency. Using code-assert it’s easy to add a Unit Test that detects this:
public class CyclicDependencyTest {
@Test
public void verifyThatThereAreNoCyclicDependencies() {
/// Create an analyzer for the whole project
AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main();
// Check that we have no CyclicDependencies
assertThat(new DependencyAnalyzer(analyzerConfig).analyze(), hasNoCycles());
}
}
Demo source code
The source code of the demo project is on GitHub: https://github.com/robbrinkman/architectural-fitness-functions