Skip to content

Commit 6b93bcc

Browse files
committed
updated ArchUnit tests - see corresponding notes
Signed-off-by: Konstantin Läufer <[email protected]>
1 parent 7b7c8d2 commit 6b93bcc

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

ARCHITECTURE_TESTS.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Architecture Tests
2+
3+
This project uses [ArchUnit](https://www.archunit.org/) to enforce architectural constraints and detect design smells.
4+
5+
## Current Architecture Tests
6+
7+
### ✅ Passing Tests
8+
9+
1. **`packages_should_be_free_of_cycles()`**
10+
- Verifies no cyclic dependencies between top-level packages
11+
- Pattern: `edu.luc.etl.cs313.android.simplestopwatch.(*)`
12+
13+
2. **`subpackages_should_be_free_of_cycles()`**
14+
- Verifies no cyclic dependencies at sub-package level
15+
- Pattern: `edu.luc.etl.cs313.android.simplestopwatch.(*).(*)`
16+
17+
### ❌ Failing Tests (Demonstrating Architecture Violations)
18+
19+
3. **`model_should_only_depend_on_java_common_or_model()`**
20+
- **Purpose**: Ensures model layer is self-contained
21+
- **Allowed Dependencies**: `java.*`, `javax.*`, `..common.*`, `..model.*`
22+
- **Current Violations** (4):
23+
- `LapRunningState.getId()``R$string.LAP_RUNNING`
24+
- `LapStoppedState.getId()``R$string.LAP_STOPPED`
25+
- `RunningState.getId()``R$string.RUNNING`
26+
- `StoppedState.getId()``R$string.STOPPED`
27+
28+
4. **`model_should_not_depend_on_R_class()`**
29+
- **Purpose**: Explicitly forbids dependencies on Android resource class `R`
30+
- **Rationale**: Model classes should not depend on UI-specific resources
31+
- **Current Violations** (4): Same as test #3 above
32+
33+
## Technical Notes
34+
35+
### Why R.java Uses `Integer.valueOf()`
36+
37+
The `R` class uses `Integer.valueOf()` instead of literal integer constants:
38+
39+
```java
40+
public static final int STOPPED = Integer.valueOf(0); // Not: = 0;
41+
```
42+
43+
**Reason**: Java's compiler performs **compile-time constant inlining** for `static final` primitive fields with literal values. This means:
44+
45+
- With `int STOPPED = 0;` → Compiler replaces `R.string.STOPPED` with `0` in bytecode
46+
- With `int STOPPED = Integer.valueOf(0);` → Bytecode contains `getstatic R$string.STOPPED`
47+
48+
ArchUnit operates on **compiled bytecode**, not source code. If constants were inlined, ArchUnit couldn't detect the dependency because it wouldn't exist in the bytecode—only in the source.
49+
50+
By using `Integer.valueOf()`, we ensure the dependency exists at runtime, making it detectable by ArchUnit.
51+
52+
### Bytecode Comparison
53+
54+
**With literal constant** (inlined):
55+
```
56+
public int getId();
57+
Code:
58+
0: iconst_0 // Push constant 0
59+
1: ireturn
60+
```
61+
62+
**With Integer.valueOf()** (not inlined):
63+
```
64+
public int getId();
65+
Code:
66+
0: getstatic #37 // Field R$string.STOPPED:I
67+
3: ireturn
68+
```
69+
70+
## Running the Tests
71+
72+
```bash
73+
# Run all architecture tests
74+
./gradlew test --tests "*.PackageDependencyTest"
75+
76+
# Run specific test
77+
./gradlew test --tests "*.PackageDependencyTest.model_should_not_depend_on_R_class"
78+
79+
# Run with detailed violation output
80+
./gradlew test --tests "*.PackageDependencyTest" --info
81+
```
82+
83+
## How to Fix the Violations
84+
85+
To eliminate the architectural violations, the `R` class constants should be moved to the `common` package or the state classes should use a different mechanism for state identification that doesn't depend on UI resources.
86+
87+
For example:
88+
1. Move constants to `Constants.java` in the `common` package
89+
2. Have states return their own type/class as identification
90+
3. Use an enum for state identification

src/test/java/edu/luc/etl/cs313/android/simplestopwatch/architecture/PackageDependencyTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package edu.luc.etl.cs313.android.simplestopwatch.architecture;
22

33
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
4+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
45

56
import org.junit.jupiter.api.Test;
67

78
import com.tngtech.archunit.core.importer.ClassFileImporter;
9+
import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars;
10+
import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests;
11+
import com.tngtech.archunit.junit.AnalyzeClasses;
812
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
913

1014
/**
1115
* Architecture tests to verify structural constraints and design rules.
1216
*
1317
* @author laufer
1418
*/
19+
@AnalyzeClasses(importOptions = {DoNotIncludeTests.class, DoNotIncludeJars.class})
1520
public class PackageDependencyTest {
1621

1722
/**
@@ -57,6 +62,7 @@ public void subpackages_should_be_free_of_cycles() {
5762
*/
5863
@Test
5964
public void model_should_only_depend_on_java_common_or_model() {
65+
// TODO look for more elegant way to import only production classes
6066
final var importedClasses =
6167
new ClassFileImporter().importPackages("edu.luc.etl.cs313.android.simplestopwatch");
6268

@@ -73,4 +79,31 @@ public void model_should_only_depend_on_java_common_or_model() {
7379
.because("Model classes should be independent of external frameworks and UI code")
7480
.check(importedClasses);
7581
}
82+
83+
/**
84+
* Ensures that model classes do not depend on the R class from the root package.
85+
* This verifies that model classes don't have UI-specific resource dependencies.
86+
*
87+
* Note: R.java uses Integer.valueOf() instead of literal constants to prevent
88+
* compile-time constant inlining, which allows ArchUnit to detect these dependencies
89+
* at the bytecode level. With literal constants, the compiler would inline them and
90+
* ArchUnit wouldn't be able to detect the dependency.
91+
*/
92+
@Test
93+
public void model_should_not_depend_on_R_class() {
94+
final var importedClasses =
95+
new ClassFileImporter().importPackages("edu.luc.etl.cs313.android.simplestopwatch");
96+
97+
noClasses()
98+
.that()
99+
.resideInAPackage("..model..")
100+
.should()
101+
.accessClassesThat()
102+
.haveFullyQualifiedName("edu.luc.etl.cs313.android.simplestopwatch.R")
103+
.orShould()
104+
.accessClassesThat()
105+
.haveNameMatching("edu\\.luc\\.etl\\.cs313\\.android\\.simplestopwatch\\.R\\$.*")
106+
.because("Model classes should not depend on Android resource class R")
107+
.check(importedClasses);
108+
}
76109
}

0 commit comments

Comments
 (0)