Skip to content

Commit 06491a7

Browse files
authored
Keep JUnit deltas when converting assertEquals, assertNotEquals and assertArrayEquals to AssertJ (#1216)
* Keep non-zero deltas on integral actuals in `JUnitAssertEqualsToAssertThat` `assertEquals(100, elapsed, 10)` with a `long elapsed` became `assertThat(elapsed).isEqualTo(100)`, silently dropping the tolerance, and `assertEquals(1.5, count, 0.5)` became an `isEqualTo` that can never pass. JUnit widens the actual to the `float` or `double` delta type, so the recipe now casts it the same way and keeps the tolerance, as in `assertThat((float) elapsed).isCloseTo(100, within((float) 10))`. The delta is only dropped when it is a literal zero and both values are `int` or `long`. * Tell deltas from messages by the resolved overload in `assertNotEquals` and `assertArrayEquals` Like `assertEquals` before #911, `JUnitAssertNotEqualsToAssertThat` and `JUnitAssertArrayEqualsToAssertThat` decided whether the third argument was a delta from that argument's own type. So `assertNotEquals(0.0, d, 0)` became `.as(0)`, and `assertNotEquals(5, count, 1.0)` called an `isNotCloseTo(int, Offset<Double>)` that doesn't exist. Both now use the resolved parameter type and cast the actual and delta where JUnit widened them, sharing the helpers with `JUnitAssertEqualsToAssertThat`.
1 parent 2706beb commit 06491a7

7 files changed

Lines changed: 208 additions & 99 deletions

File tree

‎src/main/java/org/openrewrite/java/testing/assertj/JUnitAssertArrayEqualsToAssertThat.java‎

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import org.openrewrite.java.tree.Expression;
2929
import org.openrewrite.java.tree.J;
3030
import org.openrewrite.java.tree.JavaType;
31-
import org.openrewrite.java.tree.TypeUtils;
3231

3332
import java.util.List;
3433

34+
import static org.openrewrite.java.testing.assertj.JUnitDelta.floatingPointDeltaType;
35+
import static org.openrewrite.java.testing.assertj.JUnitDelta.widenedTo;
36+
3537
public class JUnitAssertArrayEqualsToAssertThat extends Recipe {
3638

3739
private static final String JUNIT = "org.junit.jupiter.api.Assertions";
@@ -67,16 +69,8 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
6769
.build()
6870
.apply(getCursor(), md.getCoordinates().replace(), actual, expected);
6971
}
70-
if (args.size() == 3 && isFloatingPointType(args.get(2))) {
71-
maybeAddImport(ASSERTJ, "within", false);
72-
// assert is using floating points with a delta and no message.
73-
return JavaTemplate.builder("assertThat(#{anyArray()}).containsExactly(#{anyArray()}, within(#{any()}));")
74-
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
75-
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
76-
.build()
77-
.apply(getCursor(), md.getCoordinates().replace(), actual, expected, args.get(2));
78-
}
79-
if (args.size() == 3) {
72+
JavaType.Primitive deltaType = floatingPointDeltaType(md);
73+
if (deltaType == null) {
8074
Expression message = args.get(2);
8175
return JavaTemplate.builder("assertThat(#{anyArray()}).as(#{any()}).containsExactly(#{anyArray()});")
8276
.staticImports(ASSERTJ + ".assertThat")
@@ -86,31 +80,21 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
8680
}
8781

8882
maybeAddImport(ASSERTJ, "within", false);
83+
Expression delta = args.get(2);
84+
if (args.size() == 3) {
85+
return JavaTemplate.builder("assertThat(#{anyArray()}).containsExactly(#{anyArray()}, within(" + widenedTo(deltaType, delta) + "));")
86+
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
87+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
88+
.build()
89+
.apply(getCursor(), md.getCoordinates().replace(), actual, expected, delta);
90+
}
8991

90-
// The assertEquals is using a floating point with a delta argument and a message.
9192
Expression message = args.get(3);
92-
return JavaTemplate.builder("assertThat(#{anyArray()}).as(#{any()}).containsExactly(#{anyArray()}, within(#{}));")
93+
return JavaTemplate.builder("assertThat(#{anyArray()}).as(#{any()}).containsExactly(#{anyArray()}, within(" + widenedTo(deltaType, delta) + "));")
9394
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
9495
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
9596
.build()
96-
.apply(getCursor(), md.getCoordinates().replace(), actual, message, expected, args.get(2));
97-
}
98-
99-
/**
100-
* Returns true if the expression's type is either a primitive float/double or their object forms Float/Double
101-
*
102-
* @param expression The expression parsed from the original AST.
103-
* @return true if the type is a floating point number.
104-
*/
105-
private boolean isFloatingPointType(Expression expression) {
106-
JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(expression.getType());
107-
if (fullyQualified != null) {
108-
String typeName = fullyQualified.getFullyQualifiedName();
109-
return "java.lang.Double".equals(typeName) || "java.lang.Float".equals(typeName);
110-
}
111-
112-
JavaType.Primitive parameterType = TypeUtils.asPrimitive(expression.getType());
113-
return parameterType == JavaType.Primitive.Double || parameterType == JavaType.Primitive.Float;
97+
.apply(getCursor(), md.getCoordinates().replace(), actual, message, expected, delta);
11498
}
11599
});
116100
}

‎src/main/java/org/openrewrite/java/testing/assertj/JUnitAssertEqualsToAssertThat.java‎

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
import org.openrewrite.java.tree.Expression;
2929
import org.openrewrite.java.tree.J;
3030
import org.openrewrite.java.tree.JavaType;
31-
import org.openrewrite.java.tree.TypeUtils;
3231

3332
import java.util.List;
3433

34+
import static org.openrewrite.java.testing.assertj.JUnitDelta.floatingPointDeltaType;
35+
import static org.openrewrite.java.testing.assertj.JUnitDelta.isIntegralType;
36+
import static org.openrewrite.java.testing.assertj.JUnitDelta.isLiteralZero;
37+
import static org.openrewrite.java.testing.assertj.JUnitDelta.widenedTo;
38+
3539
public class JUnitAssertEqualsToAssertThat extends Recipe {
3640

3741
private static final String JUNIT = "org.junit.jupiter.api.Assertions";
@@ -82,27 +86,26 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
8286
.build()
8387
.apply(getCursor(), mi.getCoordinates().replace(), actual, message, expected);
8488
}
89+
Expression delta = args.get(2);
90+
boolean dropDelta = isLiteralZero(delta) && isIntegralType(actual) && isIntegralType(expected);
8591
if (args.size() == 3) {
86-
// When actual is integral but delta is floating-point, use isEqualTo instead of isCloseTo
87-
// to avoid type mismatch (e.g. AbstractLongAssert.isCloseTo requires Offset<Long>, not Offset<Double>)
88-
if (isIntegralType(actual)) {
92+
if (dropDelta) {
8993
return JavaTemplate.builder("assertThat(#{any()}).isEqualTo(#{any()});")
9094
.staticImports(ASSERTJ + ".assertThat")
9195
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
9296
.build()
9397
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected);
9498
}
9599
maybeAddImport(ASSERTJ, "within", false);
96-
return JavaTemplate.builder("assertThat(#{any()}).isCloseTo(#{any()}, " + withinExpression(deltaType, args.get(2)) + ");")
100+
return JavaTemplate.builder("assertThat(" + widenedTo(deltaType, actual) + ").isCloseTo(#{any()}, within(" + widenedTo(deltaType, delta) + "));")
97101
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
98102
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
99103
.build()
100-
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected, args.get(2));
104+
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected, delta);
101105
}
102106

103-
// The assertEquals is using a floating point with a delta argument and a message.
104107
Expression message = args.get(3);
105-
if (isIntegralType(actual)) {
108+
if (dropDelta) {
106109
return JavaTemplate.builder("assertThat(#{any()}).as(#{any()}).isEqualTo(#{any()});")
107110
.staticImports(ASSERTJ + ".assertThat")
108111
.imports("java.util.function.Supplier")
@@ -111,52 +114,17 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
111114
.apply(getCursor(), mi.getCoordinates().replace(), actual, message, expected);
112115
}
113116
maybeAddImport(ASSERTJ, "within", false);
114-
return JavaTemplate.builder("assertThat(#{any()}).as(#{any()}).isCloseTo(#{any()}, " + withinExpression(deltaType, args.get(2)) + ");")
117+
return JavaTemplate.builder("assertThat(" + widenedTo(deltaType, actual) + ").as(#{any()}).isCloseTo(#{any()}, within(" + widenedTo(deltaType, delta) + "));")
115118
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
116119
.imports("java.util.function.Supplier")
117120
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
118121
.build()
119-
.apply(getCursor(), mi.getCoordinates().replace(), actual, message, expected, args.get(2));
120-
}
121-
122-
private String withinExpression(JavaType.Primitive paramType, Expression delta) {
123-
// When the delta argument's expression type is narrower than the resolved parameter type
124-
// (e.g. literal `0` widened to `float` at the call site), insert an explicit cast so that
125-
// AssertJ resolves `within(...)` to the matching Offset<T> overload.
126-
JavaType argType = delta.getType();
127-
if (TypeUtils.asPrimitive(argType) == paramType) {
128-
return "within(#{any()})";
129-
}
130-
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(argType);
131-
if (fq != null && paramType.getClassName().equals(fq.getFullyQualifiedName())) {
132-
return "within(#{any()})";
133-
}
134-
return "within((" + paramType.getKeyword() + ") #{any()})";
122+
.apply(getCursor(), mi.getCoordinates().replace(), actual, message, expected, delta);
135123
}
136124

137125
private boolean isNullLiteral(Expression expression) {
138126
return J.Literal.isLiteralValue(expression.unwrap(), null);
139127
}
140-
141-
private JavaType.Primitive floatingPointDeltaType(J.MethodInvocation mi) {
142-
// Inspect the resolved method's third parameter type, not the argument's expression type:
143-
// a literal like `0` passed to `assertEquals(float, float, float)` is widened to float by the compiler.
144-
if (mi.getMethodType() == null || mi.getMethodType().getParameterTypes().size() < 3) {
145-
return null;
146-
}
147-
JavaType.Primitive primitive = TypeUtils.asPrimitive(mi.getMethodType().getParameterTypes().get(2));
148-
return primitive == JavaType.Primitive.Double || primitive == JavaType.Primitive.Float ? primitive : null;
149-
}
150-
151-
private boolean isIntegralType(Expression expression) {
152-
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(expression.getType());
153-
if (fq != null) {
154-
String typeName = fq.getFullyQualifiedName();
155-
return "java.lang.Long".equals(typeName) || "java.lang.Integer".equals(typeName);
156-
}
157-
JavaType.Primitive p = TypeUtils.asPrimitive(expression.getType());
158-
return p == JavaType.Primitive.Long || p == JavaType.Primitive.Int;
159-
}
160128
});
161129
}
162130
}

‎src/main/java/org/openrewrite/java/testing/assertj/JUnitAssertNotEqualsToAssertThat.java‎

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import org.openrewrite.java.tree.Expression;
2929
import org.openrewrite.java.tree.J;
3030
import org.openrewrite.java.tree.JavaType;
31-
import org.openrewrite.java.tree.TypeUtils;
3231

3332
import java.util.List;
3433

34+
import static org.openrewrite.java.testing.assertj.JUnitDelta.floatingPointDeltaType;
35+
import static org.openrewrite.java.testing.assertj.JUnitDelta.widenedTo;
36+
3537
public class JUnitAssertNotEqualsToAssertThat extends Recipe {
3638

3739
private static final String JUNIT = "org.junit.jupiter.api.Assertions";
@@ -72,15 +74,8 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
7274
.build()
7375
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected);
7476
}
75-
if (args.size() == 3 && isFloatingPointType(args.get(2))) {
76-
maybeAddImport(ASSERTJ, "within", false);
77-
return JavaTemplate.builder("assertThat(#{any()}).isNotCloseTo(#{any()}, within(#{any()}));")
78-
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
79-
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
80-
.build()
81-
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected, args.get(2));
82-
}
83-
if (args.size() == 3) {
77+
JavaType.Primitive deltaType = floatingPointDeltaType(mi);
78+
if (deltaType == null) {
8479
Expression message = args.get(2);
8580
return JavaTemplate.builder("assertThat(#{any()}).as(#{any()}).isNotEqualTo(#{any()});")
8681
.staticImports(ASSERTJ + ".assertThat")
@@ -90,29 +85,26 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu
9085
}
9186

9287
maybeAddImport(ASSERTJ, "within", false);
88+
Expression delta = args.get(2);
89+
if (args.size() == 3) {
90+
return JavaTemplate.builder("assertThat(" + widenedTo(deltaType, actual) + ").isNotCloseTo(#{any()}, within(" + widenedTo(deltaType, delta) + "));")
91+
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
92+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
93+
.build()
94+
.apply(getCursor(), mi.getCoordinates().replace(), actual, expected, delta);
95+
}
9396

9497
Expression message = args.get(3);
95-
return JavaTemplate.builder("assertThat(#{any()}).as(#{any()}).isNotCloseTo(#{any()}, within(#{any()}));")
98+
return JavaTemplate.builder("assertThat(" + widenedTo(deltaType, actual) + ").as(#{any()}).isNotCloseTo(#{any()}, within(" + widenedTo(deltaType, delta) + "));")
9699
.staticImports(ASSERTJ + ".assertThat", ASSERTJ + ".within")
97100
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "assertj-core-3"))
98101
.build()
99-
.apply(getCursor(), method.getCoordinates().replace(), actual, message, expected, args.get(2));
102+
.apply(getCursor(), mi.getCoordinates().replace(), actual, message, expected, delta);
100103
}
101104

102105
private boolean isNullLiteral(Expression expression) {
103106
return J.Literal.isLiteralValue(expression.unwrap(), null);
104107
}
105-
106-
private boolean isFloatingPointType(Expression expression) {
107-
JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(expression.getType());
108-
if (fullyQualified != null) {
109-
String typeName = fullyQualified.getFullyQualifiedName();
110-
return "java.lang.Double".equals(typeName) || "java.lang.Float".equals(typeName);
111-
}
112-
113-
JavaType.Primitive parameterType = TypeUtils.asPrimitive(expression.getType());
114-
return parameterType == JavaType.Primitive.Double || parameterType == JavaType.Primitive.Float;
115-
}
116108
});
117109
}
118110
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.testing.assertj;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.openrewrite.java.tree.Expression;
20+
import org.openrewrite.java.tree.J;
21+
import org.openrewrite.java.tree.JavaType;
22+
import org.openrewrite.java.tree.TypeUtils;
23+
24+
final class JUnitDelta {
25+
26+
private JUnitDelta() {
27+
}
28+
29+
static JavaType.@Nullable Primitive floatingPointDeltaType(J.MethodInvocation mi) {
30+
// The resolved parameter, not the argument type, tells a delta from a message: JUnit widens an `int` delta
31+
JavaType.Method methodType = mi.getMethodType();
32+
if (methodType == null || methodType.getParameterTypes().size() < 3) {
33+
return null;
34+
}
35+
JavaType.Primitive primitive = TypeUtils.asPrimitive(methodType.getParameterTypes().get(2));
36+
return primitive == JavaType.Primitive.Double || primitive == JavaType.Primitive.Float ? primitive : null;
37+
}
38+
39+
static boolean isIntegralType(Expression expression) {
40+
JavaType.Primitive primitive = TypeUtils.asPrimitive(expression.getType());
41+
return primitive == JavaType.Primitive.Long || primitive == JavaType.Primitive.Int;
42+
}
43+
44+
static boolean isLiteralZero(Expression expression) {
45+
Expression unwrapped = expression.unwrap();
46+
Object value = unwrapped instanceof J.Literal ? ((J.Literal) unwrapped).getValue() : null;
47+
return value instanceof Number && ((Number) value).doubleValue() == 0;
48+
}
49+
50+
static String widenedTo(JavaType.Primitive type, Expression argument) {
51+
// Cast where JUnit widened the argument, so AssertJ picks the `float` or `double` assert and `Offset<T>`
52+
if (hasType(argument, type)) {
53+
return "#{any()}";
54+
}
55+
return "(" + type.getKeyword() + ") " + (bindsLooserThanCast(argument) ? "(#{any()})" : "#{any()}");
56+
}
57+
58+
private static boolean bindsLooserThanCast(Expression expression) {
59+
return expression instanceof J.Binary || expression instanceof J.Ternary || expression instanceof J.Assignment || expression instanceof J.AssignmentOperation;
60+
}
61+
62+
private static boolean hasType(Expression expression, JavaType.Primitive type) {
63+
if (TypeUtils.asPrimitive(expression.getType()) == type) {
64+
return true;
65+
}
66+
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(expression.getType());
67+
return fq != null && type.getClassName().equals(fq.getFullyQualifiedName());
68+
}
69+
}

‎src/test/java/org/openrewrite/java/testing/assertj/JUnitAssertArrayEqualsToAssertThatTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,36 @@ private float[] notification() {
324324
);
325325
}
326326

327+
@Test
328+
void integerLiteralDelta() {
329+
//language=java
330+
rewriteRun(
331+
java(
332+
"""
333+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
334+
335+
class MyTest {
336+
void test(double[] actual) {
337+
assertArrayEquals(new double[]{1.0d}, actual, 0);
338+
assertArrayEquals(new double[]{1.0d}, actual, 0, "Should match");
339+
}
340+
}
341+
""",
342+
"""
343+
import static org.assertj.core.api.Assertions.assertThat;
344+
import static org.assertj.core.api.Assertions.within;
345+
346+
class MyTest {
347+
void test(double[] actual) {
348+
assertThat(actual).containsExactly(new double[]{1.0d}, within((double) 0));
349+
assertThat(actual).as("Should match").containsExactly(new double[]{1.0d}, within((double) 0));
350+
}
351+
}
352+
"""
353+
)
354+
);
355+
}
356+
327357
@Test
328358
void fullyQualifiedMethodWithMessage() {
329359
//language=java

0 commit comments

Comments
 (0)