Skip to content

Commit 793cca8

Browse files
juherrkrmahadevan
authored andcommitted
feat(softassert): back SoftAssert with AssertJ SoftAssertions
Replace the hand-rolled failure collection in org.testng.asserts.SoftAssert with AssertJ's SoftAssertions engine. assertAll() now aggregates failures using AssertJ's native "Multiple Failures" report; a custom assertAll(String) message is kept as a prefix, and each individual failure (with its root cause) is attached as a suppressed exception so error details remain visible. Add org.opentest4j:opentest4j as a runtime dependency (AssertJ only declares it as provided) so failures are aggregated as an AssertJMultipleFailuresError. Part of #15
1 parent 9825aba commit 793cca8

4 files changed

Lines changed: 46 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- `Assert.assertEquals(String, String, String)` now delegates to AssertJ, producing a readable
2020
character-level diff on mismatch. The failure message format changes accordingly
2121
([#18](https://github.com/testng-team/testng-asserts/issues/18)).
22+
- `SoftAssert` is now backed by AssertJ's `SoftAssertions`: collected failures are aggregated with
23+
AssertJ's native "Multiple Failures" report instead of the previous
24+
`"The following asserts failed:"` format. A custom `assertAll(String)` message is kept as a
25+
prefix, and each individual failure (with its root cause) is attached as a suppressed exception.
26+
This adds `org.opentest4j:opentest4j` as a runtime dependency (AssertJ only declares it as
27+
`provided`).
2228

2329
### Fixed
2430

pom.xml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@
6363

6464
<dependencies>
6565
<!--
66-
AssertJ is the only runtime dependency: org.testng.Assert delegates the
67-
behaviour-identical assertions (assertNull/NotNull/Same/NotSame, assertThrows) to it.
66+
AssertJ is the main runtime dependency: org.testng.Assert delegates the
67+
behaviour-identical assertions (assertNull/NotNull/Same/NotSame, assertThrows) to it,
68+
and org.testng.asserts.SoftAssert is backed by AssertJ's SoftAssertions.
69+
opentest4j is required at runtime so AssertJ aggregates soft-assertion failures as an
70+
AssertJMultipleFailuresError, which keeps each failure (and its root cause) as a
71+
suppressed exception. AssertJ only declares it as "provided", so we promote it here.
6872
TestNG is required at test scope only, to run the assertion tests
6973
(annotations, runner) and for the collection helpers used by the tests.
7074
-->
@@ -73,6 +77,11 @@
7377
<artifactId>assertj-core</artifactId>
7478
<version>3.27.7</version>
7579
</dependency>
80+
<dependency>
81+
<groupId>org.opentest4j</groupId>
82+
<artifactId>opentest4j</artifactId>
83+
<version>1.3.0</version>
84+
</dependency>
7685
<dependency>
7786
<groupId>org.testng</groupId>
7887
<artifactId>testng</artifactId>
Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package org.testng.asserts;
22

3-
import java.util.LinkedHashMap;
4-
import java.util.Map;
3+
import org.assertj.core.api.SoftAssertions;
4+
import org.opentest4j.MultipleFailuresError;
55

66
/**
77
* When an assertion fails, don't throw an exception but record the failure. Calling {@code
88
* assertAll()} will cause an exception to be thrown if at least one assertion failed.
9+
*
10+
* <p>Failures are collected and aggregated by AssertJ's {@link SoftAssertions}, so {@code
11+
* assertAll()} reports them using AssertJ's native multiple-failures format.
912
*/
1013
public class SoftAssert extends Assertion {
11-
// LinkedHashMap to preserve the order
12-
private final Map<AssertionError, IAssert<?>> m_errors = new LinkedHashMap<>();
13-
private static final String DEFAULT_SOFT_ASSERT_MESSAGE = "The following asserts failed:";
14+
private final SoftAssertions softly = new SoftAssertions();
1415

1516
@Override
1617
protected void doAssert(IAssert<?> a) {
@@ -20,7 +21,7 @@ protected void doAssert(IAssert<?> a) {
2021
onAssertSuccess(a);
2122
} catch (AssertionError ex) {
2223
onAssertFailure(a, ex);
23-
m_errors.put(ex, a);
24+
softly.collectAssertionError(ex);
2425
} finally {
2526
onAfterAssert(a);
2627
}
@@ -31,19 +32,21 @@ public void assertAll() {
3132
}
3233

3334
public void assertAll(String message) {
34-
if (!m_errors.isEmpty()) {
35-
StringBuilder sb = new StringBuilder(null == message ? DEFAULT_SOFT_ASSERT_MESSAGE : message);
36-
boolean first = true;
37-
for (AssertionError error : m_errors.keySet()) {
38-
if (first) {
39-
first = false;
40-
} else {
41-
sb.append(",");
35+
try {
36+
softly.assertAll();
37+
} catch (AssertionError e) {
38+
// AssertJ keeps each failure in MultipleFailuresError#getFailures() but does not wire them
39+
// into the stack trace. Expose them (and their root causes) as suppressed exceptions so the
40+
// underlying failure details are still surfaced by standard tooling (see GITHUB-1778).
41+
if (e instanceof MultipleFailuresError) {
42+
for (Throwable failure : ((MultipleFailuresError) e).getFailures()) {
43+
e.addSuppressed(failure);
4244
}
43-
sb.append("\n\t");
44-
sb.append(getErrorDetails(error));
4545
}
46-
throw new AssertionError(sb.toString());
46+
if (message == null) {
47+
throw e;
48+
}
49+
throw new AssertionError(message + System.lineSeparator() + e.getMessage(), e);
4750
}
4851
}
4952
}

src/test/java/test/assertion/SoftAssertTest.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,25 @@ public void onAssertFailure(IAssert assertCommand, AssertionError ex) {
3636

3737
@DataProvider(name = "messages")
3838
public Object[][] getMessages() {
39-
String hasMessage = "msg";
40-
return new Object[][] {{hasMessage, hasMessage}, {null, "The following asserts failed:"}};
39+
// A custom message is used as a prefix; a null message falls back to AssertJ's native
40+
// "Multiple Failures" aggregated report.
41+
return new Object[][] {{"msg", "msg"}, {null, "Multiple Failures"}};
4142
}
4243

4344
@Test(dataProvider = "messages")
44-
public void testDefaultMessage(String actualMsg, String expectedMsg) {
45+
public void testDefaultMessage(String actualMsg, String expectedToken) {
4546
try {
4647
final SoftAssert sa = new SoftAssert();
4748
sa.assertTrue(false);
4849
sa.assertAll(actualMsg);
4950
Assert.fail();
5051
} catch (AssertionError exc) {
5152
Assert.assertNotNull(exc.getMessage());
52-
Assert.assertTrue(exc.getMessage().startsWith(expectedMsg));
53+
Assert.assertTrue(exc.getMessage().contains(expectedToken), exc.getMessage());
54+
if (actualMsg != null) {
55+
// A custom message is reported as a prefix of the aggregated failure report.
56+
Assert.assertTrue(exc.getMessage().startsWith(actualMsg), exc.getMessage());
57+
}
5358
}
5459
}
5560

0 commit comments

Comments
 (0)