Skip to content

Commit e936f18

Browse files
committed
JUnit Assertion Library
1 parent 2127ae2 commit e936f18

6 files changed

Lines changed: 371 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Notable project changes are documented here.
66

77
### Added
88

9+
- JUnit-friendly fluent assertions for fingerprints
910
- Failure dependency graphs generated from throwable causal chains
1011

1112
## 1.1.1 - 2026-06-18

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ BUGDNA-7A3F21
1616
- Deterministic exception fingerprints
1717
- PII-safe normalization for emails and numeric identifiers
1818
- Fingerprint knowledge base lookup for owners and runbooks
19+
- Fluent JUnit-friendly fingerprint assertions
1920
- Root-cause, category, stability, and priority analysis
2021
- Root-cause family clustering across different fingerprints
2122
- Failure dependency graphs from causal chains
@@ -107,6 +108,16 @@ System.out.println(fingerprint.getId());
107108
System.out.println(fingerprint.explain());
108109
```
109110

111+
Assert fingerprints in automated tests:
112+
113+
```java
114+
import static io.github.bugdna.BugDnaAssertions.assertThat;
115+
116+
assertThat(fingerprint)
117+
.hasCategory(FailureCategory.DATABASE)
118+
.hasRootCause(SQLTimeoutException.class);
119+
```
120+
110121
Render causal dependencies:
111122

112123
```java
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package io.github.bugdna;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Fluent assertions for BugDNA fingerprints in automated tests.
7+
*/
8+
public final class BugDnaAssertions {
9+
10+
private BugDnaAssertions() {
11+
}
12+
13+
/**
14+
* Starts a fluent assertion chain for a fingerprint.
15+
*
16+
* @param fingerprint fingerprint under test
17+
* @return fingerprint assertion
18+
* @throws AssertionError when {@code fingerprint} is {@code null}
19+
*/
20+
public static FingerprintAssert assertThat(Fingerprint fingerprint) {
21+
return new FingerprintAssert(fingerprint);
22+
}
23+
24+
/**
25+
* Fluent assertions for {@link Fingerprint}.
26+
*/
27+
public static final class FingerprintAssert {
28+
29+
private final Fingerprint fingerprint;
30+
31+
private FingerprintAssert(Fingerprint fingerprint) {
32+
if (fingerprint == null) {
33+
throw new AssertionError("Expected fingerprint to be non-null.");
34+
}
35+
this.fingerprint = fingerprint;
36+
}
37+
38+
/**
39+
* Verifies the fingerprint category.
40+
*
41+
* @param expected expected category
42+
* @return this assertion
43+
*/
44+
public FingerprintAssert hasCategory(FailureCategory expected) {
45+
FailureCategory requiredExpected = Objects.requireNonNull(
46+
expected,
47+
"expected must not be null"
48+
);
49+
if (fingerprint.getCategory() != requiredExpected) {
50+
fail("category", requiredExpected, fingerprint.getCategory());
51+
}
52+
return this;
53+
}
54+
55+
/**
56+
* Verifies the fingerprint family.
57+
*
58+
* @param expected expected family
59+
* @return this assertion
60+
*/
61+
public FingerprintAssert hasFamily(FailureFamily expected) {
62+
FailureFamily requiredExpected = Objects.requireNonNull(
63+
expected,
64+
"expected must not be null"
65+
);
66+
if (fingerprint.getFamily() != requiredExpected) {
67+
fail("family", requiredExpected, fingerprint.getFamily());
68+
}
69+
return this;
70+
}
71+
72+
/**
73+
* Verifies the root-cause exception class.
74+
*
75+
* @param expected expected root-cause class
76+
* @return this assertion
77+
*/
78+
public FingerprintAssert hasRootCause(Class<? extends Throwable> expected) {
79+
Class<? extends Throwable> requiredExpected = Objects.requireNonNull(
80+
expected,
81+
"expected must not be null"
82+
);
83+
return hasRootCause(requiredExpected.getName());
84+
}
85+
86+
/**
87+
* Verifies the root-cause class name.
88+
*
89+
* @param expected expected root-cause class name
90+
* @return this assertion
91+
*/
92+
public FingerprintAssert hasRootCause(String expected) {
93+
String requiredExpected = Objects.requireNonNull(
94+
expected,
95+
"expected must not be null"
96+
);
97+
if (!fingerprint.getRootCause().equals(requiredExpected)) {
98+
fail("root cause", requiredExpected, fingerprint.getRootCause());
99+
}
100+
return this;
101+
}
102+
103+
/**
104+
* Verifies the fingerprint ID.
105+
*
106+
* @param expected expected fingerprint ID
107+
* @return this assertion
108+
*/
109+
public FingerprintAssert hasId(String expected) {
110+
String requiredExpected = Objects.requireNonNull(
111+
expected,
112+
"expected must not be null"
113+
);
114+
if (!fingerprint.getId().equals(requiredExpected)) {
115+
fail("id", requiredExpected, fingerprint.getId());
116+
}
117+
return this;
118+
}
119+
120+
/**
121+
* Verifies the simple origin signature.
122+
*
123+
* @param expected expected signature
124+
* @return this assertion
125+
*/
126+
public FingerprintAssert hasSignature(String expected) {
127+
String requiredExpected = Objects.requireNonNull(
128+
expected,
129+
"expected must not be null"
130+
);
131+
if (!fingerprint.getSignature().equals(requiredExpected)) {
132+
fail("signature", requiredExpected, fingerprint.getSignature());
133+
}
134+
return this;
135+
}
136+
137+
/**
138+
* Verifies the qualified origin signature.
139+
*
140+
* @param expected expected qualified signature
141+
* @return this assertion
142+
*/
143+
public FingerprintAssert hasQualifiedSignature(String expected) {
144+
String requiredExpected = Objects.requireNonNull(
145+
expected,
146+
"expected must not be null"
147+
);
148+
if (!fingerprint.getQualifiedSignature().equals(requiredExpected)) {
149+
fail(
150+
"qualified signature",
151+
requiredExpected,
152+
fingerprint.getQualifiedSignature()
153+
);
154+
}
155+
return this;
156+
}
157+
158+
/**
159+
* Verifies the stability score.
160+
*
161+
* @param expected expected stability score
162+
* @return this assertion
163+
*/
164+
public FingerprintAssert hasStabilityScore(int expected) {
165+
if (fingerprint.getStabilityScore() != expected) {
166+
fail("stability score", Integer.valueOf(expected), Integer.valueOf(
167+
fingerprint.getStabilityScore()
168+
));
169+
}
170+
return this;
171+
}
172+
173+
/**
174+
* Returns the asserted fingerprint for custom checks.
175+
*
176+
* @return asserted fingerprint
177+
*/
178+
public Fingerprint actual() {
179+
return fingerprint;
180+
}
181+
182+
private static void fail(String field, Object expected, Object actual) {
183+
throw new AssertionError(
184+
"Expected fingerprint " + field + " to be <" + expected
185+
+ "> but was <" + actual + ">."
186+
);
187+
}
188+
}
189+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.github.bugdna;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.sql.SQLTimeoutException;
6+
7+
import static io.github.bugdna.BugDnaAssertions.assertThat;
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertSame;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
class BugDnaAssertionsTest {
14+
15+
@Test
16+
void verifiesFingerprintWithFluentAssertions() {
17+
Fingerprint fingerprint = BugDna.generate(failureAt(
18+
new SQLTimeoutException("query timed out"),
19+
"com.example.UserRepository",
20+
"find",
21+
10
22+
));
23+
24+
assertSame(
25+
fingerprint,
26+
assertThat(fingerprint)
27+
.hasCategory(FailureCategory.DATABASE)
28+
.hasFamily(FailureFamily.DATABASE_CONNECTIVITY)
29+
.hasRootCause(SQLTimeoutException.class)
30+
.hasRootCause("java.sql.SQLTimeoutException")
31+
.hasId(fingerprint.getId())
32+
.hasSignature("UserRepository#find")
33+
.hasQualifiedSignature("com.example.UserRepository#find")
34+
.hasStabilityScore(90)
35+
.actual()
36+
);
37+
}
38+
39+
@Test
40+
void failsWithHelpfulMessageWhenValuesDiffer() {
41+
Fingerprint fingerprint = BugDna.generate(failureAt(
42+
new SQLTimeoutException("query timed out"),
43+
"com.example.UserRepository",
44+
"find",
45+
10
46+
));
47+
48+
AssertionError error = assertThrows(
49+
AssertionError.class,
50+
() -> assertThat(fingerprint).hasCategory(FailureCategory.NETWORK)
51+
);
52+
53+
assertEquals(
54+
"Expected fingerprint category to be <NETWORK> but was <DATABASE>.",
55+
error.getMessage()
56+
);
57+
}
58+
59+
@Test
60+
void rejectsNullFingerprintAsAssertionFailure() {
61+
AssertionError error = assertThrows(
62+
AssertionError.class,
63+
() -> assertThat(null)
64+
);
65+
66+
assertEquals("Expected fingerprint to be non-null.", error.getMessage());
67+
}
68+
69+
@Test
70+
void rejectsNullExpectedValues() {
71+
Fingerprint fingerprint = BugDna.generate(failureAt(
72+
new IllegalArgumentException("invalid"),
73+
"com.example.UserValidator",
74+
"validate",
75+
10
76+
));
77+
BugDnaAssertions.FingerprintAssert assertion = assertThat(fingerprint);
78+
Class<? extends Throwable> nullRootCauseClass = null;
79+
String nullValue = null;
80+
81+
assertThrows(NullPointerException.class, () -> assertion.hasCategory(null));
82+
assertThrows(NullPointerException.class, () -> assertion.hasFamily(null));
83+
assertThrows(NullPointerException.class, () -> assertion.hasRootCause(nullRootCauseClass));
84+
assertThrows(NullPointerException.class, () -> assertion.hasRootCause(nullValue));
85+
assertThrows(NullPointerException.class, () -> assertion.hasId(nullValue));
86+
assertThrows(NullPointerException.class, () -> assertion.hasSignature(nullValue));
87+
assertThrows(NullPointerException.class, () -> assertion.hasQualifiedSignature(nullValue));
88+
}
89+
90+
@Test
91+
void reportsFailuresForEveryAssertionField() {
92+
Fingerprint fingerprint = BugDna.generate(failureAt(
93+
new IllegalArgumentException("invalid"),
94+
"com.example.UserValidator",
95+
"validate",
96+
10
97+
));
98+
99+
verifyFails(() -> assertThat(fingerprint).hasFamily(FailureFamily.DATABASE_OPERATION), "family");
100+
verifyFails(() -> assertThat(fingerprint).hasRootCause(IllegalStateException.class), "root cause");
101+
verifyFails(() -> assertThat(fingerprint).hasRootCause("example.OtherException"), "root cause");
102+
verifyFails(() -> assertThat(fingerprint).hasId("BUGDNA-OTHER"), "id");
103+
verifyFails(() -> assertThat(fingerprint).hasSignature("Other#method"), "signature");
104+
verifyFails(
105+
() -> assertThat(fingerprint).hasQualifiedSignature("example.Other#method"),
106+
"qualified signature"
107+
);
108+
verifyFails(() -> assertThat(fingerprint).hasStabilityScore(98), "stability score");
109+
}
110+
111+
private static void verifyFails(Runnable assertion, String field) {
112+
AssertionError error = assertThrows(AssertionError.class, assertion::run);
113+
114+
assertTrue(error.getMessage().contains("Expected fingerprint " + field));
115+
}
116+
117+
private static Throwable failureAt(
118+
Throwable failure,
119+
String className,
120+
String methodName,
121+
int lineNumber
122+
) {
123+
failure.setStackTrace(new StackTraceElement[] {
124+
new StackTraceElement(className, methodName, className + ".java", lineNumber)
125+
});
126+
return failure;
127+
}
128+
}

docs/api-reference.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ Fingerprint fingerprint = BugDna.generate(failure);
1212
System.out.println(fingerprint.explain());
1313
```
1414

15+
Assert fingerprints in tests:
16+
17+
```java
18+
import static io.github.bugdna.BugDnaAssertions.assertThat;
19+
20+
assertThat(fingerprint)
21+
.hasCategory(FailureCategory.DATABASE)
22+
.hasRootCause(SQLTimeoutException.class);
23+
```
24+
1525
Render a causal dependency graph:
1626

1727
```java
@@ -76,6 +86,22 @@ Similarity similarity = BugSimilarity.compare(
7686
- `loadKnowledgeBase(Map<String, FingerprintKnowledge>)`
7787
- `readKnowledgeBase(Path)`
7888

89+
### `BugDnaAssertions`
90+
91+
- `assertThat(Fingerprint)`
92+
93+
### `BugDnaAssertions.FingerprintAssert`
94+
95+
- `hasCategory(FailureCategory)`
96+
- `hasFamily(FailureFamily)`
97+
- `hasRootCause(Class<? extends Throwable>)`
98+
- `hasRootCause(String)`
99+
- `hasId(String)`
100+
- `hasSignature(String)`
101+
- `hasQualifiedSignature(String)`
102+
- `hasStabilityScore(int)`
103+
- `actual()`
104+
79105
### `FingerprintKnowledge`
80106

81107
- Identity: `getId()`

0 commit comments

Comments
 (0)