You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by et...@apache.org on 2025/05/14 08:00:13 UTC

(iceberg) branch main updated: Spark 3.4: Migrate other JUnit 4 dependant tests to JUnit 5 (#13044)

This is an automated email from the ASF dual-hosted git repository.

etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new d44cba57c8 Spark 3.4: Migrate other JUnit 4 dependant tests to JUnit 5 (#13044)
d44cba57c8 is described below

commit d44cba57c859fef04eec1dcc26838b6331c33702
Author: Tom Tanaka <43...@users.noreply.github.com>
AuthorDate: Wed May 14 17:00:07 2025 +0900

    Spark 3.4: Migrate other JUnit 4 dependant tests to JUnit 5 (#13044)
---
 .../apache/iceberg/spark/SparkTestHelperBase.java  |  16 +-
 .../iceberg/spark/TestChangelogIterator.java       |  16 +-
 .../org/apache/iceberg/spark/TestSparkFilters.java |  95 +++--
 .../apache/iceberg/spark/TestSparkSchemaUtil.java  |  34 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  35 +-
 .../apache/iceberg/spark/TestSparkV2Filters.java   | 137 ++++---
 .../iceberg/spark/TestSparkValueConverter.java     |  12 +-
 .../iceberg/spark/source/TestReadProjection.java   | 429 ++++++++++++---------
 .../spark/source/TestSnapshotSelection.java        | 180 +++++----
 .../iceberg/spark/source/TestSparkDataWrite.java   | 225 ++++++-----
 .../spark/source/TestSparkReadProjection.java      |  47 ++-
 .../iceberg/spark/source/TestSparkTable.java       |   7 +-
 .../apache/iceberg/spark/SparkTestHelperBase.java  |   6 +-
 .../org/apache/iceberg/spark/TestSparkFilters.java |  42 +-
 .../apache/iceberg/spark/TestSparkSchemaUtil.java  |   7 +-
 .../apache/iceberg/spark/TestSparkTableUtil.java   |  18 +-
 .../apache/iceberg/spark/TestSparkV2Filters.java   |  67 ++--
 .../iceberg/spark/source/TestReadProjection.java   | 116 +++---
 .../spark/source/TestSnapshotSelection.java        |  25 +-
 .../iceberg/spark/source/TestSparkDataWrite.java   |  88 ++---
 20 files changed, 871 insertions(+), 731 deletions(-)

diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
index 97484702ca..022702d853 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
@@ -18,11 +18,12 @@
  */
 package org.apache.iceberg.spark;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.apache.spark.sql.Row;
-import org.junit.Assert;
 
 public class SparkTestHelperBase {
   protected static final Object ANY = new Object();
@@ -55,12 +56,13 @@ public class SparkTestHelperBase {
 
   protected void assertEquals(
       String context, List<Object[]> expectedRows, List<Object[]> actualRows) {
-    Assert.assertEquals(
-        context + ": number of results should match", expectedRows.size(), actualRows.size());
+    assertThat(actualRows)
+        .as("%s: number of results should match", context)
+        .hasSameSizeAs(expectedRows);
     for (int row = 0; row < expectedRows.size(); row += 1) {
       Object[] expected = expectedRows.get(row);
       Object[] actual = actualRows.get(row);
-      Assert.assertEquals("Number of columns should match", expected.length, actual.length);
+      assertThat(actual).as("Number of columns should match").hasSameSizeAs(expected);
       for (int col = 0; col < actualRows.get(row).length; col += 1) {
         String newContext = String.format("%s: row %d col %d", context, row + 1, col + 1);
         assertEquals(newContext, expected, actual);
@@ -69,19 +71,19 @@ public class SparkTestHelperBase {
   }
 
   protected void assertEquals(String context, Object[] expectedRow, Object[] actualRow) {
-    Assert.assertEquals("Number of columns should match", expectedRow.length, actualRow.length);
+    assertThat(actualRow).as("Number of columns should match").hasSameSizeAs(expectedRow);
     for (int col = 0; col < actualRow.length; col += 1) {
       Object expectedValue = expectedRow[col];
       Object actualValue = actualRow[col];
       if (expectedValue != null && expectedValue.getClass().isArray()) {
         String newContext = String.format("%s (nested col %d)", context, col + 1);
         if (expectedValue instanceof byte[]) {
-          Assert.assertArrayEquals(newContext, (byte[]) expectedValue, (byte[]) actualValue);
+          assertThat(actualValue).as(newContext).isEqualTo(expectedValue);
         } else {
           assertEquals(newContext, (Object[]) expectedValue, (Object[]) actualValue);
         }
       } else if (expectedValue != ANY) {
-        Assert.assertEquals(context + " contents should match", expectedValue, actualValue);
+        assertThat(actualValue).as("%s contents should match", context).isEqualTo(expectedValue);
       }
     }
   }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestChangelogIterator.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestChangelogIterator.java
index 0539598f14..bd9832f7d6 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestChangelogIterator.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestChangelogIterator.java
@@ -18,7 +18,8 @@
  */
 package org.apache.iceberg.spark;
 
-import static org.junit.Assert.assertThrows;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -33,8 +34,7 @@ import org.apache.spark.sql.types.DataTypes;
 import org.apache.spark.sql.types.Metadata;
 import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestChangelogIterator extends SparkTestHelperBase {
   private static final String DELETE = ChangelogOperation.DELETE.name();
@@ -78,7 +78,7 @@ public class TestChangelogIterator extends SparkTestHelperBase {
         Arrays.asList(RowType.DELETED, RowType.INSERTED, RowType.CARRY_OVER, RowType.UPDATED),
         0,
         permutations);
-    Assert.assertEquals(24, permutations.size());
+    assertThat(permutations).hasSize(24);
 
     for (Object[] permutation : permutations) {
       validate(permutation);
@@ -196,10 +196,10 @@ public class TestChangelogIterator extends SparkTestHelperBase {
     Iterator<Row> iterator =
         ChangelogIterator.computeUpdates(rowsWithDuplication.iterator(), SCHEMA, IDENTIFIER_FIELDS);
 
-    assertThrows(
-        "Cannot compute updates because there are multiple rows with the same identifier fields([id, name]). Please make sure the rows are unique.",
-        IllegalStateException.class,
-        () -> Lists.newArrayList(iterator));
+    assertThatThrownBy(() -> Lists.newArrayList(iterator))
+        .isInstanceOf(IllegalStateException.class)
+        .hasMessage(
+            "Cannot compute updates because there are multiple rows with the same identifier fields([id,name]). Please make sure the rows are unique.");
 
     // still allow extra insert rows
     rowsWithDuplication =
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
index b1f8cf61a7..bc3f407d09 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
@@ -18,6 +18,8 @@
  */
 package org.apache.iceberg.spark;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import java.sql.Date;
 import java.sql.Timestamp;
 import java.time.Instant;
@@ -40,8 +42,7 @@ import org.apache.spark.sql.sources.IsNull;
 import org.apache.spark.sql.sources.LessThan;
 import org.apache.spark.sql.sources.LessThanOrEqual;
 import org.apache.spark.sql.sources.Not;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestSparkFilters {
 
@@ -59,54 +60,68 @@ public class TestSparkFilters {
           IsNull isNull = IsNull.apply(quoted);
           Expression expectedIsNull = Expressions.isNull(unquoted);
           Expression actualIsNull = SparkFilters.convert(isNull);
-          Assert.assertEquals(
-              "IsNull must match", expectedIsNull.toString(), actualIsNull.toString());
+          assertThat(actualIsNull)
+              .asString()
+              .as("IsNull must match")
+              .isEqualTo(expectedIsNull.toString());
 
           IsNotNull isNotNull = IsNotNull.apply(quoted);
           Expression expectedIsNotNull = Expressions.notNull(unquoted);
           Expression actualIsNotNull = SparkFilters.convert(isNotNull);
-          Assert.assertEquals(
-              "IsNotNull must match", expectedIsNotNull.toString(), actualIsNotNull.toString());
+          assertThat(actualIsNotNull)
+              .asString()
+              .as("IsNotNull must match")
+              .isEqualTo(expectedIsNotNull.toString());
 
           LessThan lt = LessThan.apply(quoted, 1);
           Expression expectedLt = Expressions.lessThan(unquoted, 1);
           Expression actualLt = SparkFilters.convert(lt);
-          Assert.assertEquals("LessThan must match", expectedLt.toString(), actualLt.toString());
+          assertThat(actualLt)
+              .asString()
+              .as("LessThan must match")
+              .isEqualTo(expectedLt.toString());
 
           LessThanOrEqual ltEq = LessThanOrEqual.apply(quoted, 1);
           Expression expectedLtEq = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualLtEq = SparkFilters.convert(ltEq);
-          Assert.assertEquals(
-              "LessThanOrEqual must match", expectedLtEq.toString(), actualLtEq.toString());
+          assertThat(actualLtEq)
+              .asString()
+              .as("LessThanOrEqual must match")
+              .isEqualTo(expectedLtEq.toString());
 
           GreaterThan gt = GreaterThan.apply(quoted, 1);
           Expression expectedGt = Expressions.greaterThan(unquoted, 1);
           Expression actualGt = SparkFilters.convert(gt);
-          Assert.assertEquals("GreaterThan must match", expectedGt.toString(), actualGt.toString());
+          assertThat(actualGt)
+              .asString()
+              .as("GreaterThan must match")
+              .isEqualTo(expectedGt.toString());
 
           GreaterThanOrEqual gtEq = GreaterThanOrEqual.apply(quoted, 1);
           Expression expectedGtEq = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualGtEq = SparkFilters.convert(gtEq);
-          Assert.assertEquals(
-              "GreaterThanOrEqual must match", expectedGtEq.toString(), actualGtEq.toString());
+          assertThat(actualGtEq)
+              .asString()
+              .as("GreaterThanOrEqual must match")
+              .isEqualTo(expectedGtEq.toString());
 
           EqualTo eq = EqualTo.apply(quoted, 1);
           Expression expectedEq = Expressions.equal(unquoted, 1);
           Expression actualEq = SparkFilters.convert(eq);
-          Assert.assertEquals("EqualTo must match", expectedEq.toString(), actualEq.toString());
+          assertThat(actualEq).asString().as("EqualTo must match").isEqualTo(expectedEq.toString());
 
           EqualNullSafe eqNullSafe = EqualNullSafe.apply(quoted, 1);
           Expression expectedEqNullSafe = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe = SparkFilters.convert(eqNullSafe);
-          Assert.assertEquals(
-              "EqualNullSafe must match",
-              expectedEqNullSafe.toString(),
-              actualEqNullSafe.toString());
+          assertThat(actualEqNullSafe)
+              .asString()
+              .as("EqualNullSafe must match")
+              .isEqualTo(expectedEqNullSafe.toString());
 
           In in = In.apply(quoted, new Integer[] {1});
           Expression expectedIn = Expressions.in(unquoted, 1);
           Expression actualIn = SparkFilters.convert(in);
-          Assert.assertEquals("In must match", expectedIn.toString(), actualIn.toString());
+          assertThat(actualIn).asString().as("In must match").isEqualTo(expectedIn.toString());
         });
   }
 
@@ -120,14 +135,14 @@ public class TestSparkFilters {
     Expression timestampExpression = SparkFilters.convert(GreaterThan.apply("x", timestamp));
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    Assert.assertEquals(
-        "Generated Timestamp expression should be correct",
-        rawExpression.toString(),
-        timestampExpression.toString());
-    Assert.assertEquals(
-        "Generated Instant expression should be correct",
-        rawExpression.toString(),
-        instantExpression.toString());
+    assertThat(timestampExpression)
+        .asString()
+        .as("Generated Timestamp expression should be correct")
+        .isEqualTo(rawExpression.toString());
+    assertThat(instantExpression)
+        .asString()
+        .as("Generated Instant expression should be correct")
+        .isEqualTo(rawExpression.toString());
   }
 
   @Test
@@ -139,10 +154,10 @@ public class TestSparkFilters {
     Expression instantExpression = SparkFilters.convert(GreaterThan.apply("x", ldt));
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    Assert.assertEquals(
-        "Generated Instant expression should be correct",
-        rawExpression.toString(),
-        instantExpression.toString());
+    assertThat(instantExpression)
+        .asString()
+        .as("Generated Instant expression should be correct")
+        .isEqualTo(rawExpression.toString());
   }
 
   @Test
@@ -155,15 +170,15 @@ public class TestSparkFilters {
     Expression dateExpression = SparkFilters.convert(GreaterThan.apply("x", date));
     Expression rawExpression = Expressions.greaterThan("x", epochDay);
 
-    Assert.assertEquals(
-        "Generated localdate expression should be correct",
-        rawExpression.toString(),
-        localDateExpression.toString());
+    assertThat(localDateExpression)
+        .asString()
+        .as("Generated localdate expression should be correct")
+        .isEqualTo(rawExpression.toString());
 
-    Assert.assertEquals(
-        "Generated date expression should be correct",
-        rawExpression.toString(),
-        dateExpression.toString());
+    assertThat(dateExpression)
+        .asString()
+        .as("Generated date expression should be correct")
+        .isEqualTo(rawExpression.toString());
   }
 
   @Test
@@ -171,7 +186,7 @@ public class TestSparkFilters {
     Not filter =
         Not.apply(And.apply(EqualTo.apply("col1", 1), In.apply("col2", new Integer[] {1, 2})));
     Expression converted = SparkFilters.convert(filter);
-    Assert.assertNull("Expression should not be converted", converted);
+    assertThat(converted).as("Expression should not be converted").isNull();
   }
 
   @Test
@@ -180,6 +195,6 @@ public class TestSparkFilters {
     Expression actual = SparkFilters.convert(filter);
     Expression expected =
         Expressions.and(Expressions.notNull("col"), Expressions.notIn("col", 1, 2));
-    Assert.assertEquals("Expressions should match", expected.toString(), actual.toString());
+    assertThat(actual).asString().as("Expressions should match").isEqualTo(expected.toString());
   }
 }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
index 259f7c3dd7..9b5b207a5b 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
@@ -19,8 +19,8 @@
 package org.apache.iceberg.spark;
 
 import static org.apache.iceberg.types.Types.NestedField.optional;
+import static org.assertj.core.api.Assertions.assertThat;
 
-import java.io.IOException;
 import java.util.List;
 import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.Schema;
@@ -28,8 +28,7 @@ import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.catalyst.expressions.AttributeReference;
 import org.apache.spark.sql.catalyst.expressions.MetadataAttribute;
 import org.apache.spark.sql.types.StructType;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestSparkSchemaUtil {
   private static final Schema TEST_SCHEMA =
@@ -44,24 +43,23 @@ public class TestSparkSchemaUtil {
           MetadataColumns.ROW_POSITION);
 
   @Test
-  public void testEstimateSizeMaxValue() throws IOException {
-    Assert.assertEquals(
-        "estimateSize returns Long max value",
-        Long.MAX_VALUE,
-        SparkSchemaUtil.estimateSize(null, Long.MAX_VALUE));
+  public void testEstimateSizeMaxValue() {
+    assertThat(SparkSchemaUtil.estimateSize(null, Long.MAX_VALUE))
+        .as("estimateSize returns Long max value")
+        .isEqualTo(Long.MAX_VALUE);
   }
 
   @Test
-  public void testEstimateSizeWithOverflow() throws IOException {
+  public void testEstimateSizeWithOverflow() {
     long tableSize =
         SparkSchemaUtil.estimateSize(SparkSchemaUtil.convert(TEST_SCHEMA), Long.MAX_VALUE - 1);
-    Assert.assertEquals("estimateSize handles overflow", Long.MAX_VALUE, tableSize);
+    assertThat(tableSize).as("estimateSize handles overflow").isEqualTo(Long.MAX_VALUE);
   }
 
   @Test
-  public void testEstimateSize() throws IOException {
+  public void testEstimateSize() {
     long tableSize = SparkSchemaUtil.estimateSize(SparkSchemaUtil.convert(TEST_SCHEMA), 1);
-    Assert.assertEquals("estimateSize matches with expected approximation", 24, tableSize);
+    assertThat(tableSize).as("estimateSize matches with expected approximation").isEqualTo(24);
   }
 
   @Test
@@ -71,13 +69,13 @@ public class TestSparkSchemaUtil {
         scala.collection.JavaConverters.seqAsJavaList(structType.toAttributes());
     for (AttributeReference attrRef : attrRefs) {
       if (MetadataColumns.isMetadataColumn(attrRef.name())) {
-        Assert.assertTrue(
-            "metadata columns should have __metadata_col in attribute metadata",
-            MetadataAttribute.unapply(attrRef).isDefined());
+        assertThat(MetadataAttribute.unapply(attrRef).isDefined())
+            .as("metadata columns should have __metadata_col in attribute metadata")
+            .isTrue();
       } else {
-        Assert.assertFalse(
-            "non metadata columns should not have __metadata_col in attribute metadata",
-            MetadataAttribute.unapply(attrRef).isDefined());
+        assertThat(MetadataAttribute.unapply(attrRef).isDefined())
+            .as("non metadata columns should not have __metadata_col in attribute metadata")
+            .isFalse();
       }
     }
   }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index 51c8d9643c..93e4c89687 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -29,8 +29,7 @@ import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.spark.SparkTableUtil.SparkPartition;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestSparkTableUtil {
   @Test
@@ -69,13 +68,15 @@ public class TestSparkTableUtil {
     MetricsConfig config = MetricsConfig.fromProperties(metricsConfig);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
-    Assert.assertEquals(
-        MetricsModes.Full.get().toString(), deserialized.columnMode("col1").toString());
-    Assert.assertEquals(
-        MetricsModes.Truncate.withLength(16).toString(),
-        deserialized.columnMode("col2").toString());
-    Assert.assertEquals(
-        MetricsModes.Counts.get().toString(), deserialized.columnMode("col3").toString());
+    assertThat(deserialized.columnMode("col1"))
+        .asString()
+        .isEqualTo(MetricsModes.Full.get().toString());
+    assertThat(deserialized.columnMode("col2"))
+        .asString()
+        .isEqualTo(MetricsModes.Truncate.withLength(16).toString());
+    assertThat(deserialized.columnMode("col3"))
+        .asString()
+        .isEqualTo(MetricsModes.Counts.get().toString());
   }
 
   @Test
@@ -92,12 +93,14 @@ public class TestSparkTableUtil {
     MetricsConfig config = MetricsConfig.fromProperties(metricsConfig);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
-    Assert.assertEquals(
-        MetricsModes.Full.get().toString(), deserialized.columnMode("col1").toString());
-    Assert.assertEquals(
-        MetricsModes.Truncate.withLength(16).toString(),
-        deserialized.columnMode("col2").toString());
-    Assert.assertEquals(
-        MetricsModes.Counts.get().toString(), deserialized.columnMode("col3").toString());
+    assertThat(deserialized.columnMode("col1"))
+        .asString()
+        .isEqualTo(MetricsModes.Full.get().toString());
+    assertThat(deserialized.columnMode("col2"))
+        .asString()
+        .isEqualTo(MetricsModes.Truncate.withLength(16).toString());
+    assertThat(deserialized.columnMode("col3"))
+        .asString()
+        .isEqualTo(MetricsModes.Counts.get().toString());
   }
 }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
index eae8a6ec02..e0b590e5a6 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
@@ -52,8 +52,7 @@ import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.DataTypes;
 import org.apache.spark.sql.types.StructType;
 import org.apache.spark.unsafe.types.UTF8String;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestSparkV2Filters {
 
@@ -90,98 +89,130 @@ public class TestSparkV2Filters {
           Predicate isNull = new Predicate("IS_NULL", attrOnly);
           Expression expectedIsNull = Expressions.isNull(unquoted);
           Expression actualIsNull = SparkV2Filters.convert(isNull);
-          Assert.assertEquals(
-              "IsNull must match", expectedIsNull.toString(), actualIsNull.toString());
+          assertThat(actualIsNull)
+              .asString()
+              .as("IsNull must match")
+              .isEqualTo(expectedIsNull.toString());
 
           Predicate isNotNull = new Predicate("IS_NOT_NULL", attrOnly);
           Expression expectedIsNotNull = Expressions.notNull(unquoted);
           Expression actualIsNotNull = SparkV2Filters.convert(isNotNull);
-          Assert.assertEquals(
-              "IsNotNull must match", expectedIsNotNull.toString(), actualIsNotNull.toString());
+          assertThat(actualIsNotNull)
+              .asString()
+              .as("IsNotNull must match")
+              .isEqualTo(expectedIsNotNull.toString());
 
           Predicate lt1 = new Predicate("<", attrAndValue);
           Expression expectedLt1 = Expressions.lessThan(unquoted, 1);
           Expression actualLt1 = SparkV2Filters.convert(lt1);
-          Assert.assertEquals("LessThan must match", expectedLt1.toString(), actualLt1.toString());
+          assertThat(actualLt1)
+              .asString()
+              .as("LessThan must match")
+              .isEqualTo(expectedLt1.toString());
 
           Predicate lt2 = new Predicate("<", valueAndAttr);
           Expression expectedLt2 = Expressions.greaterThan(unquoted, 1);
           Expression actualLt2 = SparkV2Filters.convert(lt2);
-          Assert.assertEquals("LessThan must match", expectedLt2.toString(), actualLt2.toString());
+          assertThat(actualLt2)
+              .asString()
+              .as("LessThan must match")
+              .isEqualTo(expectedLt2.toString());
 
           Predicate ltEq1 = new Predicate("<=", attrAndValue);
           Expression expectedLtEq1 = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualLtEq1 = SparkV2Filters.convert(ltEq1);
-          Assert.assertEquals(
-              "LessThanOrEqual must match", expectedLtEq1.toString(), actualLtEq1.toString());
+          assertThat(actualLtEq1)
+              .asString()
+              .as("LessThanOrEqual must match")
+              .isEqualTo(expectedLtEq1.toString());
 
           Predicate ltEq2 = new Predicate("<=", valueAndAttr);
           Expression expectedLtEq2 = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualLtEq2 = SparkV2Filters.convert(ltEq2);
-          Assert.assertEquals(
-              "LessThanOrEqual must match", expectedLtEq2.toString(), actualLtEq2.toString());
+          assertThat(actualLtEq2)
+              .asString()
+              .as("LessThanOrEqual must match")
+              .isEqualTo(expectedLtEq2.toString());
 
           Predicate gt1 = new Predicate(">", attrAndValue);
           Expression expectedGt1 = Expressions.greaterThan(unquoted, 1);
           Expression actualGt1 = SparkV2Filters.convert(gt1);
-          Assert.assertEquals(
-              "GreaterThan must match", expectedGt1.toString(), actualGt1.toString());
+          assertThat(actualGt1)
+              .asString()
+              .as("GreaterThan must match")
+              .isEqualTo(expectedGt1.toString());
 
           Predicate gt2 = new Predicate(">", valueAndAttr);
           Expression expectedGt2 = Expressions.lessThan(unquoted, 1);
           Expression actualGt2 = SparkV2Filters.convert(gt2);
-          Assert.assertEquals(
-              "GreaterThan must match", expectedGt2.toString(), actualGt2.toString());
+          assertThat(actualGt2)
+              .asString()
+              .as("GreaterThan must match")
+              .isEqualTo(expectedGt2.toString());
 
           Predicate gtEq1 = new Predicate(">=", attrAndValue);
           Expression expectedGtEq1 = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualGtEq1 = SparkV2Filters.convert(gtEq1);
-          Assert.assertEquals(
-              "GreaterThanOrEqual must match", expectedGtEq1.toString(), actualGtEq1.toString());
+          assertThat(actualGtEq1)
+              .asString()
+              .as("GreaterThanOrEqual must match")
+              .isEqualTo(expectedGtEq1.toString());
 
           Predicate gtEq2 = new Predicate(">=", valueAndAttr);
           Expression expectedGtEq2 = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualGtEq2 = SparkV2Filters.convert(gtEq2);
-          Assert.assertEquals(
-              "GreaterThanOrEqual must match", expectedGtEq2.toString(), actualGtEq2.toString());
+          assertThat(actualGtEq2)
+              .asString()
+              .as("GreaterThanOrEqual must match")
+              .isEqualTo(expectedGtEq2.toString());
 
           Predicate eq1 = new Predicate("=", attrAndValue);
           Expression expectedEq1 = Expressions.equal(unquoted, 1);
           Expression actualEq1 = SparkV2Filters.convert(eq1);
-          Assert.assertEquals("EqualTo must match", expectedEq1.toString(), actualEq1.toString());
+          assertThat(actualEq1)
+              .asString()
+              .as("EqualTo must match")
+              .isEqualTo(expectedEq1.toString());
 
           Predicate eq2 = new Predicate("=", valueAndAttr);
           Expression expectedEq2 = Expressions.equal(unquoted, 1);
           Expression actualEq2 = SparkV2Filters.convert(eq2);
-          Assert.assertEquals("EqualTo must match", expectedEq2.toString(), actualEq2.toString());
+          assertThat(actualEq2)
+              .asString()
+              .as("EqualTo must match")
+              .isEqualTo(expectedEq2.toString());
 
           Predicate notEq1 = new Predicate("<>", attrAndValue);
           Expression expectedNotEq1 = Expressions.notEqual(unquoted, 1);
           Expression actualNotEq1 = SparkV2Filters.convert(notEq1);
-          Assert.assertEquals(
-              "NotEqualTo must match", expectedNotEq1.toString(), actualNotEq1.toString());
+          assertThat(actualNotEq1)
+              .asString()
+              .as("NotEqualTo must match")
+              .isEqualTo(expectedNotEq1.toString());
 
           Predicate notEq2 = new Predicate("<>", valueAndAttr);
           Expression expectedNotEq2 = Expressions.notEqual(unquoted, 1);
           Expression actualNotEq2 = SparkV2Filters.convert(notEq2);
-          Assert.assertEquals(
-              "NotEqualTo must match", expectedNotEq2.toString(), actualNotEq2.toString());
+          assertThat(actualNotEq2)
+              .asString()
+              .as("NotEqualTo must match")
+              .isEqualTo(expectedNotEq2.toString());
 
           Predicate eqNullSafe1 = new Predicate("<=>", attrAndValue);
           Expression expectedEqNullSafe1 = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe1 = SparkV2Filters.convert(eqNullSafe1);
-          Assert.assertEquals(
-              "EqualNullSafe must match",
-              expectedEqNullSafe1.toString(),
-              actualEqNullSafe1.toString());
+          assertThat(actualEqNullSafe1)
+              .asString()
+              .as("EqualNullSafe must match")
+              .isEqualTo(expectedEqNullSafe1.toString());
 
           Predicate eqNullSafe2 = new Predicate("<=>", valueAndAttr);
           Expression expectedEqNullSafe2 = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe2 = SparkV2Filters.convert(eqNullSafe2);
-          Assert.assertEquals(
-              "EqualNullSafe must match",
-              expectedEqNullSafe2.toString(),
-              actualEqNullSafe2.toString());
+          assertThat(actualEqNullSafe2)
+              .asString()
+              .as("EqualNullSafe must match")
+              .isEqualTo(expectedEqNullSafe2.toString());
 
           LiteralValue str =
               new LiteralValue(UTF8String.fromString("iceberg"), DataTypes.StringType);
@@ -190,18 +221,20 @@ public class TestSparkV2Filters {
           Predicate startsWith = new Predicate("STARTS_WITH", attrAndStr);
           Expression expectedStartsWith = Expressions.startsWith(unquoted, "iceberg");
           Expression actualStartsWith = SparkV2Filters.convert(startsWith);
-          Assert.assertEquals(
-              "StartsWith must match", expectedStartsWith.toString(), actualStartsWith.toString());
+          assertThat(actualStartsWith)
+              .asString()
+              .as("StartsWith must match")
+              .isEqualTo(expectedStartsWith.toString());
 
           Predicate in = new Predicate("IN", attrAndValue);
           Expression expectedIn = Expressions.in(unquoted, 1);
           Expression actualIn = SparkV2Filters.convert(in);
-          Assert.assertEquals("In must match", expectedIn.toString(), actualIn.toString());
+          assertThat(actualIn).asString().as("In must match").isEqualTo(expectedIn.toString());
 
           Predicate and = new And(lt1, eq1);
           Expression expectedAnd = Expressions.and(expectedLt1, expectedEq1);
           Expression actualAnd = SparkV2Filters.convert(and);
-          Assert.assertEquals("And must match", expectedAnd.toString(), actualAnd.toString());
+          assertThat(actualAnd).asString().as("And must match").isEqualTo(expectedAnd.toString());
 
           org.apache.spark.sql.connector.expressions.Expression[] attrAndAttr =
               new org.apache.spark.sql.connector.expressions.Expression[] {
@@ -210,21 +243,21 @@ public class TestSparkV2Filters {
           Predicate invalid = new Predicate("<", attrAndAttr);
           Predicate andWithInvalidLeft = new And(invalid, eq1);
           Expression convertedAnd = SparkV2Filters.convert(andWithInvalidLeft);
-          Assert.assertEquals("And must match", convertedAnd, null);
+          assertThat(convertedAnd).as("And must match").isNull();
 
           Predicate or = new Or(lt1, eq1);
           Expression expectedOr = Expressions.or(expectedLt1, expectedEq1);
           Expression actualOr = SparkV2Filters.convert(or);
-          Assert.assertEquals("Or must match", expectedOr.toString(), actualOr.toString());
+          assertThat(actualOr).asString().as("Or must match").isEqualTo(expectedOr.toString());
 
           Predicate orWithInvalidLeft = new Or(invalid, eq1);
           Expression convertedOr = SparkV2Filters.convert(orWithInvalidLeft);
-          Assert.assertEquals("Or must match", convertedOr, null);
+          assertThat(convertedOr).as("Or must match").isNull();
 
           Predicate not = new Not(lt1);
           Expression expectedNot = Expressions.not(expectedLt1);
           Expression actualNot = SparkV2Filters.convert(not);
-          Assert.assertEquals("Not must match", expectedNot.toString(), actualNot.toString());
+          assertThat(actualNot).asString().as("Not must match").isEqualTo(expectedNot.toString());
         });
   }
 
@@ -380,10 +413,10 @@ public class TestSparkV2Filters {
     Expression tsExpression = SparkV2Filters.convert(predicate);
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    Assert.assertEquals(
-        "Generated Timestamp expression should be correct",
-        rawExpression.toString(),
-        tsExpression.toString());
+    assertThat(tsExpression)
+        .asString()
+        .as("Generated Timestamp expression should be correct")
+        .isEqualTo(rawExpression.toString());
   }
 
   @Test
@@ -400,10 +433,10 @@ public class TestSparkV2Filters {
     Expression dateExpression = SparkV2Filters.convert(predicate);
     Expression rawExpression = Expressions.greaterThan("x", epochDay);
 
-    Assert.assertEquals(
-        "Generated date expression should be correct",
-        rawExpression.toString(),
-        dateExpression.toString());
+    assertThat(dateExpression)
+        .asString()
+        .as("Generated date expression should be correct")
+        .isEqualTo(rawExpression.toString());
   }
 
   @Test
@@ -422,7 +455,7 @@ public class TestSparkV2Filters {
 
     Not filter = new Not(new And(equal, in));
     Expression converted = SparkV2Filters.convert(filter);
-    Assert.assertNull("Expression should not be converted", converted);
+    assertThat(converted).as("Expression should not be converted").isNull();
   }
 
   @Test
@@ -439,7 +472,7 @@ public class TestSparkV2Filters {
     Expression actual = SparkV2Filters.convert(not);
     Expression expected =
         Expressions.and(Expressions.notNull("col"), Expressions.notIn("col", 1, 2));
-    Assert.assertEquals("Expressions should match", expected.toString(), actual.toString());
+    assertThat(actual).asString().as("Expressions should match").isEqualTo(expected.toString());
   }
 
   @Test
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkValueConverter.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkValueConverter.java
index 7f00c7edd8..c7a2e6c18f 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkValueConverter.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/TestSparkValueConverter.java
@@ -18,14 +18,15 @@
  */
 package org.apache.iceberg.spark;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.data.GenericRecord;
 import org.apache.iceberg.data.Record;
 import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.RowFactory;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 public class TestSparkValueConverter {
   @Test
@@ -86,9 +87,8 @@ public class TestSparkValueConverter {
     Row sparkRow = RowFactory.create(1, null);
     Record record = GenericRecord.create(schema);
     record.set(0, 1);
-    Assert.assertEquals(
-        "Round-trip conversion should produce original value",
-        record,
-        SparkValueConverter.convert(schema, sparkRow));
+    assertThat(SparkValueConverter.convert(schema, sparkRow))
+        .as("Round-trip conversion should produce original value")
+        .isEqualTo(record);
   }
 }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
index b3f9423c47..81de071762 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
@@ -19,11 +19,17 @@
 package org.apache.iceberg.spark.source;
 
 import static org.apache.avro.Schema.Type.UNION;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.within;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
+import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.data.GenericRecord;
 import org.apache.iceberg.data.Record;
@@ -31,27 +37,23 @@ import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
-import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.types.Comparators;
 import org.apache.iceberg.types.Types;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
 
+@ExtendWith(ParameterizedTestExtension.class)
 public abstract class TestReadProjection {
-  final String format;
-
-  TestReadProjection(String format) {
-    this.format = format;
-  }
+  @Parameter(index = 0)
+  protected FileFormat format;
 
   protected abstract Record writeAndRead(
       String desc, Schema writeSchema, Schema readSchema, Record record) throws IOException;
 
-  @Rule public TemporaryFolder temp = new TemporaryFolder();
+  @TempDir protected Path temp;
 
-  @Test
+  @TestTemplate
   public void testFullProjection() throws Exception {
     Schema schema =
         new Schema(
@@ -64,15 +66,16 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("full_projection", schema, schema, record);
 
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
 
     int cmp =
         Comparators.charSequences().compare("test", (CharSequence) projected.getField("data"));
-    Assert.assertEquals("Should contain the correct data value", 0, cmp);
+    assertThat(cmp).as("Should contain the correct data value").isEqualTo(0);
   }
 
-  @Test
+  @TestTemplate
   public void testReorderedFullProjection() throws Exception {
     //    Assume.assumeTrue(
     //        "Spark's Parquet read support does not support reordered columns",
@@ -94,11 +97,14 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("reordered_full_projection", schema, reordered, record);
 
-    Assert.assertEquals("Should contain the correct 0 value", "test", projected.get(0).toString());
-    Assert.assertEquals("Should contain the correct 1 value", 34L, projected.get(1));
+    assertThat(projected.get(0))
+        .asString()
+        .as("Should contain the correct 0 value")
+        .isEqualTo("test");
+    assertThat(projected.get(1)).as("Should contain the correct 1 value").isEqualTo(34L);
   }
 
-  @Test
+  @TestTemplate
   public void testReorderedProjection() throws Exception {
     //    Assume.assumeTrue(
     //        "Spark's Parquet read support does not support reordered columns",
@@ -121,12 +127,14 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("reordered_projection", schema, reordered, record);
 
-    Assert.assertNull("Should contain the correct 0 value", projected.get(0));
-    Assert.assertEquals("Should contain the correct 1 value", "test", projected.get(1).toString());
-    Assert.assertNull("Should contain the correct 2 value", projected.get(2));
+    assertThat(projected.get(0)).as("Should contain the correct 0 value").isNull();
+    assertThat(projected.get(1).toString())
+        .as("Should contain the correct 1 value")
+        .isEqualTo("test");
+    assertThat(projected.get(2)).as("Should contain the correct 2 value").isNull();
   }
 
-  @Test
+  @TestTemplate
   @SuppressWarnings("checkstyle:AssertThatThrownByWithMessageCheck")
   public void testEmptyProjection() throws Exception {
     Schema schema =
@@ -140,13 +148,13 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("empty_projection", schema, schema.select(), record);
 
-    Assert.assertNotNull("Should read a non-null record", projected);
+    assertThat(projected).as("Should read a non-null record").isNotNull();
     // this is expected because there are no values
     // no check on the underlying error msg as it might be missing based on the JDK version
     assertThatThrownBy(() -> projected.get(0)).isInstanceOf(ArrayIndexOutOfBoundsException.class);
   }
 
-  @Test
+  @TestTemplate
   public void testBasicProjection() throws Exception {
     Schema writeSchema =
         new Schema(
@@ -160,21 +168,22 @@ public abstract class TestReadProjection {
     Schema idOnly = new Schema(Types.NestedField.required(0, "id", Types.LongType.get()));
 
     Record projected = writeAndRead("basic_projection_id", writeSchema, idOnly, record);
-    Assert.assertNull("Should not project data", projected.getField("data"));
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
+    assertThat(projected.getField("data")).as("Should not project data").isNull();
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
 
     Schema dataOnly = new Schema(Types.NestedField.optional(1, "data", Types.StringType.get()));
 
     projected = writeAndRead("basic_projection_data", writeSchema, dataOnly, record);
 
-    Assert.assertNull("Should not project id", projected.getField("id"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
     int cmp =
         Comparators.charSequences().compare("test", (CharSequence) projected.getField("data"));
-    Assert.assertEquals("Should contain the correct data value", 0, cmp);
+    assertThat(cmp).as("Should contain the correct data value").isEqualTo(0);
   }
 
-  @Test
+  @TestTemplate
   public void testRename() throws Exception {
     Schema writeSchema =
         new Schema(
@@ -192,14 +201,15 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("project_and_rename", writeSchema, readSchema, record);
 
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
     int cmp =
         Comparators.charSequences().compare("test", (CharSequence) projected.getField("renamed"));
-    Assert.assertEquals("Should contain the correct data/renamed value", 0, cmp);
+    assertThat(cmp).as("Should contain the correct data/renamed value").isEqualTo(0);
   }
 
-  @Test
+  @TestTemplate
   public void testNestedStructProjection() throws Exception {
     Schema writeSchema =
         new Schema(
@@ -222,9 +232,10 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("id_only", writeSchema, idOnly, record);
     Record projectedLocation = (Record) projected.getField("location");
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
-    Assert.assertNull("Should not project location", projectedLocation);
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
+    assertThat(projectedLocation).as("Should not project location").isNull();
 
     Schema latOnly =
         new Schema(
@@ -235,14 +246,12 @@ public abstract class TestReadProjection {
 
     projected = writeAndRead("latitude_only", writeSchema, latOnly, record);
     projectedLocation = (Record) projected.getField("location");
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project location", projected.getField("location"));
-    Assert.assertNull("Should not project longitude", projectedLocation.getField("long"));
-    Assert.assertEquals(
-        "Should project latitude",
-        52.995143f,
-        (float) projectedLocation.getField("lat"),
-        0.000001f);
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("location")).as("Should project location").isNotNull();
+    assertThat(projectedLocation.getField("long")).as("Should not project longitude").isNull();
+    assertThat((float) projectedLocation.getField("lat"))
+        .as("Should project latitude")
+        .isCloseTo(52.995143f, within(0.000001f));
 
     Schema longOnly =
         new Schema(
@@ -253,33 +262,28 @@ public abstract class TestReadProjection {
 
     projected = writeAndRead("longitude_only", writeSchema, longOnly, record);
     projectedLocation = (Record) projected.getField("location");
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project location", projected.getField("location"));
-    Assert.assertNull("Should not project latitutde", projectedLocation.getField("lat"));
-    Assert.assertEquals(
-        "Should project longitude",
-        -1.539054f,
-        (float) projectedLocation.getField("long"),
-        0.000001f);
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("location")).as("Should project location").isNotNull();
+    assertThat(projectedLocation.getField("lat")).as("Should not project latitude").isNull();
+    assertThat((float) projectedLocation.getField("long"))
+        .as("Should project longitude")
+        .isCloseTo(-1.539054f, within(0.000001f));
 
     Schema locationOnly = writeSchema.select("location");
     projected = writeAndRead("location_only", writeSchema, locationOnly, record);
     projectedLocation = (Record) projected.getField("location");
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project location", projected.getField("location"));
-    Assert.assertEquals(
-        "Should project latitude",
-        52.995143f,
-        (float) projectedLocation.getField("lat"),
-        0.000001f);
-    Assert.assertEquals(
-        "Should project longitude",
-        -1.539054f,
-        (float) projectedLocation.getField("long"),
-        0.000001f);
+
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("location")).as("Should project location").isNotNull();
+    assertThat((float) projectedLocation.getField("lat"))
+        .as("Should project latitude")
+        .isCloseTo(52.995143f, within(0.000001f));
+    assertThat((float) projectedLocation.getField("long"))
+        .as("Should project longitude")
+        .isCloseTo(-1.539054f, within(0.000001f));
   }
 
-  @Test
+  @TestTemplate
   public void testMapProjection() throws IOException {
     Schema writeSchema =
         new Schema(
@@ -298,33 +302,31 @@ public abstract class TestReadProjection {
     Schema idOnly = new Schema(Types.NestedField.required(0, "id", Types.LongType.get()));
 
     Record projected = writeAndRead("id_only", writeSchema, idOnly, record);
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
-    Assert.assertNull("Should not project properties map", projected.getField("properties"));
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
+    assertThat(projected.getField("properties")).as("Should not project properties map").isNull();
 
     Schema keyOnly = writeSchema.select("properties.key");
     projected = writeAndRead("key_only", writeSchema, keyOnly, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals(
-        "Should project entire map",
-        properties,
-        toStringMap((Map) projected.getField("properties")));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(toStringMap((Map) projected.getField("properties")))
+        .as("Should project entire map")
+        .isEqualTo(properties);
 
     Schema valueOnly = writeSchema.select("properties.value");
     projected = writeAndRead("value_only", writeSchema, valueOnly, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals(
-        "Should project entire map",
-        properties,
-        toStringMap((Map) projected.getField("properties")));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(toStringMap((Map) projected.getField("properties")))
+        .as("Should project entire map")
+        .isEqualTo(properties);
 
     Schema mapOnly = writeSchema.select("properties");
     projected = writeAndRead("map_only", writeSchema, mapOnly, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals(
-        "Should project entire map",
-        properties,
-        toStringMap((Map) projected.getField("properties")));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(toStringMap((Map) projected.getField("properties")))
+        .as("Should project entire map")
+        .isEqualTo(properties);
   }
 
   private Map<String, ?> toStringMap(Map<?, ?> map) {
@@ -339,7 +341,7 @@ public abstract class TestReadProjection {
     return stringMap;
   }
 
-  @Test
+  @TestTemplate
   public void testMapOfStructsProjection() throws IOException {
     Schema writeSchema =
         new Schema(
@@ -368,51 +370,57 @@ public abstract class TestReadProjection {
     Schema idOnly = new Schema(Types.NestedField.required(0, "id", Types.LongType.get()));
 
     Record projected = writeAndRead("id_only", writeSchema, idOnly, record);
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
-    Assert.assertNull("Should not project locations map", projected.getField("locations"));
+    assertThat(34L)
+        .as("Should contain the correct id value")
+        .isEqualTo((long) projected.getField("id"));
+    assertThat(projected.getField("locations")).as("Should not project locations map").isNull();
 
     projected = writeAndRead("all_locations", writeSchema, writeSchema.select("locations"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals(
-        "Should project locations map",
-        record.getField("locations"),
-        toStringMap((Map) projected.getField("locations")));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(toStringMap((Map) projected.getField("locations")))
+        .as("Should project locations map")
+        .isEqualTo(record.getField("locations"));
 
     projected = writeAndRead("lat_only", writeSchema, writeSchema.select("locations.lat"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+
     Map<String, ?> locations = toStringMap((Map) projected.getField("locations"));
-    Assert.assertNotNull("Should project locations map", locations);
-    Assert.assertEquals(
-        "Should contain L1 and L2", Sets.newHashSet("L1", "L2"), locations.keySet());
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
+
     Record projectedL1 = (Record) locations.get("L1");
-    Assert.assertNotNull("L1 should not be null", projectedL1);
-    Assert.assertEquals(
-        "L1 should contain lat", 53.992811f, (float) projectedL1.getField("lat"), 0.000001);
-    Assert.assertNull("L1 should not contain long", projectedL1.getField("long"));
+    assertThat(projectedL1).as("L1 should not be null").isNotNull();
+    assertThat((float) projectedL1.getField("lat"))
+        .as("L1 should contain lat")
+        .isCloseTo(53.992811f, within(0.000001f));
+    assertThat(projectedL1.getField("long")).as("L1 should not contain long").isNull();
+
     Record projectedL2 = (Record) locations.get("L2");
-    Assert.assertNotNull("L2 should not be null", projectedL2);
-    Assert.assertEquals(
-        "L2 should contain lat", 52.995143f, (float) projectedL2.getField("lat"), 0.000001);
-    Assert.assertNull("L2 should not contain long", projectedL2.getField("long"));
+    assertThat(projectedL2).as("L2 should not be null").isNotNull();
+    assertThat((float) projectedL2.getField("lat"))
+        .as("L2 should contain lat")
+        .isCloseTo(52.995143f, within(0.000001f));
+    assertThat(projectedL2.getField("long")).as("L2 should not contain long").isNull();
 
     projected =
         writeAndRead("long_only", writeSchema, writeSchema.select("locations.long"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+
     locations = toStringMap((Map) projected.getField("locations"));
-    Assert.assertNotNull("Should project locations map", locations);
-    Assert.assertEquals(
-        "Should contain L1 and L2", Sets.newHashSet("L1", "L2"), locations.keySet());
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
+
     projectedL1 = (Record) locations.get("L1");
-    Assert.assertNotNull("L1 should not be null", projectedL1);
-    Assert.assertNull("L1 should not contain lat", projectedL1.getField("lat"));
-    Assert.assertEquals(
-        "L1 should contain long", -1.542616f, (float) projectedL1.getField("long"), 0.000001);
+    assertThat(projectedL1).as("L1 should not be null").isNotNull();
+    assertThat(projectedL1.getField("lat")).as("L1 should not contain lat").isNull();
+    assertThat((float) projectedL1.getField("long"))
+        .as("L1 should contain long")
+        .isCloseTo(-1.542616f, within(0.000001f));
+
     projectedL2 = (Record) locations.get("L2");
-    Assert.assertNotNull("L2 should not be null", projectedL2);
-    Assert.assertNull("L2 should not contain lat", projectedL2.getField("lat"));
-    Assert.assertEquals(
-        "L2 should contain long", -1.539054f, (float) projectedL2.getField("long"), 0.000001);
+    assertThat(projectedL2).as("L2 should not be null").isNotNull();
+    assertThat(projectedL2.getField("lat")).as("L2 should not contain lat").isNull();
+    assertThat((float) projectedL2.getField("long"))
+        .as("L2 should contain long")
+        .isCloseTo(-1.539054f, within(0.000001f));
 
     Schema latitiudeRenamed =
         new Schema(
@@ -427,32 +435,28 @@ public abstract class TestReadProjection {
                         Types.NestedField.required(1, "latitude", Types.FloatType.get())))));
 
     projected = writeAndRead("latitude_renamed", writeSchema, latitiudeRenamed, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
     locations = toStringMap((Map) projected.getField("locations"));
-    Assert.assertNotNull("Should project locations map", locations);
-    Assert.assertEquals(
-        "Should contain L1 and L2", Sets.newHashSet("L1", "L2"), locations.keySet());
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
+
     projectedL1 = (Record) locations.get("L1");
-    Assert.assertNotNull("L1 should not be null", projectedL1);
-    Assert.assertEquals(
-        "L1 should contain latitude",
-        53.992811f,
-        (float) projectedL1.getField("latitude"),
-        0.000001);
-    Assert.assertNull("L1 should not contain lat", projectedL1.getField("lat"));
-    Assert.assertNull("L1 should not contain long", projectedL1.getField("long"));
+    assertThat(projectedL1).as("L1 should not be null").isNotNull();
+    assertThat((float) projectedL1.getField("latitude"))
+        .as("L1 should contain latitude")
+        .isCloseTo(53.992811f, within(0.000001f));
+    assertThat(projectedL1.getField("lat")).as("L1 should not contain lat").isNull();
+    assertThat(projectedL1.getField("long")).as("L1 should not contain long").isNull();
+
     projectedL2 = (Record) locations.get("L2");
-    Assert.assertNotNull("L2 should not be null", projectedL2);
-    Assert.assertEquals(
-        "L2 should contain latitude",
-        52.995143f,
-        (float) projectedL2.getField("latitude"),
-        0.000001);
-    Assert.assertNull("L2 should not contain lat", projectedL2.getField("lat"));
-    Assert.assertNull("L2 should not contain long", projectedL2.getField("long"));
+    assertThat(projectedL2).as("L2 should not be null").isNotNull();
+    assertThat((float) projectedL2.getField("latitude"))
+        .as("L2 should contain latitude")
+        .isCloseTo(52.995143f, within(0.000001f));
+    assertThat(projectedL2.getField("lat")).as("L2 should not contain lat").isNull();
+    assertThat(projectedL2.getField("long")).as("L2 should not contain long").isNull();
   }
 
-  @Test
+  @TestTemplate
   public void testListProjection() throws IOException {
     Schema writeSchema =
         new Schema(
@@ -469,22 +473,23 @@ public abstract class TestReadProjection {
     Schema idOnly = new Schema(Types.NestedField.required(0, "id", Types.LongType.get()));
 
     Record projected = writeAndRead("id_only", writeSchema, idOnly, record);
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
-    Assert.assertNull("Should not project values list", projected.getField("values"));
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
+    assertThat(projected.getField("values")).as("Should not project values list").isNull();
 
     Schema elementOnly = writeSchema.select("values.element");
     projected = writeAndRead("element_only", writeSchema, elementOnly, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals("Should project entire list", values, projected.getField("values"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("values")).as("Should project entire list").isEqualTo(values);
 
     Schema listOnly = writeSchema.select("values");
     projected = writeAndRead("list_only", writeSchema, listOnly, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals("Should project entire list", values, projected.getField("values"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("values")).as("Should project entire list").isEqualTo(values);
   }
 
-  @Test
+  @TestTemplate
   @SuppressWarnings("unchecked")
   public void testListOfStructsProjection() throws IOException {
     Schema writeSchema =
@@ -512,38 +517,58 @@ public abstract class TestReadProjection {
     Schema idOnly = new Schema(Types.NestedField.required(0, "id", Types.LongType.get()));
 
     Record projected = writeAndRead("id_only", writeSchema, idOnly, record);
-    Assert.assertEquals(
-        "Should contain the correct id value", 34L, (long) projected.getField("id"));
-    Assert.assertNull("Should not project points list", projected.getField("points"));
+    assertThat((long) projected.getField("id"))
+        .as("Should contain the correct id value")
+        .isEqualTo(34L);
+    assertThat(projected.getField("points")).as("Should not project points list").isNull();
 
     projected = writeAndRead("all_points", writeSchema, writeSchema.select("points"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertEquals(
-        "Should project points list", record.getField("points"), projected.getField("points"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("points"))
+        .as("Should project points list")
+        .isEqualTo(record.getField("points"));
 
     projected = writeAndRead("x_only", writeSchema, writeSchema.select("points.x"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project points list", projected.getField("points"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("points")).as("Should project points list").isNotNull();
+
     List<Record> points = (List<Record>) projected.getField("points");
-    Assert.assertEquals("Should read 2 points", 2, points.size());
-    Record projectedP1 = points.get(0);
-    Assert.assertEquals("Should project x", 1, (int) projectedP1.getField("x"));
-    Assert.assertNull("Should not project y", projectedP1.getField("y"));
-    Record projectedP2 = points.get(1);
-    Assert.assertEquals("Should project x", 3, (int) projectedP2.getField("x"));
-    Assert.assertNull("Should not project y", projectedP2.getField("y"));
+    assertThat(points).as("Should read 2 points").hasSize(2);
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat((int) projectedP1.getField("x")).isEqualTo(1);
+              assertThat(projectedP1.getField("y")).isNull();
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat((int) projectedP2.getField("x")).isEqualTo(3);
+              assertThat(projectedP2.getField("y")).isNull();
+            });
 
     projected = writeAndRead("y_only", writeSchema, writeSchema.select("points.y"), record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project points list", projected.getField("points"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("points")).as("Should project points list").isNotNull();
+
     points = (List<Record>) projected.getField("points");
-    Assert.assertEquals("Should read 2 points", 2, points.size());
-    projectedP1 = points.get(0);
-    Assert.assertNull("Should not project x", projectedP1.getField("x"));
-    Assert.assertEquals("Should project y", 2, (int) projectedP1.getField("y"));
-    projectedP2 = points.get(1);
-    Assert.assertNull("Should not project x", projectedP2.getField("x"));
-    Assert.assertNull("Should project null y", projectedP2.getField("y"));
+    assertThat(points).as("Should read 2 points").hasSize(2);
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat(projectedP1.getField("x")).isNull();
+              assertThat((int) projectedP1.getField("y")).isEqualTo(2);
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat(projectedP2.getField("x")).isNull();
+              assertThat(projectedP2.getField("y")).isNull();
+            });
 
     Schema yRenamed =
         new Schema(
@@ -556,18 +581,27 @@ public abstract class TestReadProjection {
                         Types.NestedField.optional(18, "z", Types.IntegerType.get())))));
 
     projected = writeAndRead("y_renamed", writeSchema, yRenamed, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project points list", projected.getField("points"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("points")).as("Should project points list").isNotNull();
+
     points = (List<Record>) projected.getField("points");
-    Assert.assertEquals("Should read 2 points", 2, points.size());
-    projectedP1 = points.get(0);
-    Assert.assertNull("Should not project x", projectedP1.getField("x"));
-    Assert.assertNull("Should not project y", projectedP1.getField("y"));
-    Assert.assertEquals("Should project z", 2, (int) projectedP1.getField("z"));
-    projectedP2 = points.get(1);
-    Assert.assertNull("Should not project x", projectedP2.getField("x"));
-    Assert.assertNull("Should not project y", projectedP2.getField("y"));
-    Assert.assertNull("Should project null z", projectedP2.getField("z"));
+    assertThat(points).as("Should read 2 points").hasSize(2);
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat(projectedP1.getField("x")).isNull();
+              assertThat(projectedP1.getField("y")).isNull();
+              assertThat((int) projectedP1.getField("z")).isEqualTo(2);
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat(projectedP2.getField("x")).isNull();
+              assertThat(projectedP2.getField("y")).isNull();
+              assertThat(projectedP2.getField("z")).isNull();
+            });
 
     Schema zAdded =
         new Schema(
@@ -582,18 +616,27 @@ public abstract class TestReadProjection {
                         Types.NestedField.optional(20, "z", Types.IntegerType.get())))));
 
     projected = writeAndRead("z_added", writeSchema, zAdded, record);
-    Assert.assertNull("Should not project id", projected.getField("id"));
-    Assert.assertNotNull("Should project points list", projected.getField("points"));
+    assertThat(projected.getField("id")).as("Should not project id").isNull();
+    assertThat(projected.getField("points")).as("Should project points list").isNotNull();
+
     points = (List<Record>) projected.getField("points");
-    Assert.assertEquals("Should read 2 points", 2, points.size());
-    projectedP1 = points.get(0);
-    Assert.assertEquals("Should project x", 1, (int) projectedP1.getField("x"));
-    Assert.assertEquals("Should project y", 2, (int) projectedP1.getField("y"));
-    Assert.assertNull("Should contain null z", projectedP1.getField("z"));
-    projectedP2 = points.get(1);
-    Assert.assertEquals("Should project x", 3, (int) projectedP2.getField("x"));
-    Assert.assertNull("Should project null y", projectedP2.getField("y"));
-    Assert.assertNull("Should contain null z", projectedP2.getField("z"));
+    assertThat(points).as("Should read 2 points").hasSize(2);
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat((int) projectedP1.getField("x")).isEqualTo(1);
+              assertThat((int) projectedP1.getField("y")).isEqualTo(2);
+              assertThat(projectedP1.getField("z")).isNull();
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat((int) projectedP2.getField("x")).isEqualTo(3);
+              assertThat(projectedP2.getField("y")).isNull();
+              assertThat(projectedP2.getField("z")).isNull();
+            });
   }
 
   private static org.apache.avro.Schema fromOption(org.apache.avro.Schema schema) {
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
index 9fc576dde5..3d74b91359 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
@@ -24,19 +24,20 @@ import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
 import org.apache.iceberg.PartitionSpec;
-import org.apache.iceberg.PlanningMode;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.hadoop.HadoopTables;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
-import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.spark.SparkReadOptions;
 import org.apache.iceberg.spark.SparkSchemaUtil;
@@ -46,21 +47,29 @@ import org.apache.spark.sql.Encoders;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.RowFactory;
 import org.apache.spark.sql.SparkSession;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+@ExtendWith(ParameterizedTestExtension.class)
 public class TestSnapshotSelection {
 
-  @Parameterized.Parameters(name = "planningMode = {0}")
+  @Parameters(name = "properties = {0}")
   public static Object[] parameters() {
-    return new Object[] {LOCAL, DISTRIBUTED};
+    return new Object[][] {
+      {
+        ImmutableMap.of(
+            TableProperties.DATA_PLANNING_MODE, LOCAL.modeName(),
+            TableProperties.DELETE_PLANNING_MODE, LOCAL.modeName())
+      },
+      {
+        ImmutableMap.of(
+            TableProperties.DATA_PLANNING_MODE, DISTRIBUTED.modeName(),
+            TableProperties.DELETE_PLANNING_MODE, DISTRIBUTED.modeName())
+      }
+    };
   }
 
   private static final Configuration CONF = new Configuration();
@@ -68,34 +77,28 @@ public class TestSnapshotSelection {
       new Schema(
           optional(1, "id", Types.IntegerType.get()), optional(2, "data", Types.StringType.get()));
 
-  @Rule public TemporaryFolder temp = new TemporaryFolder();
+  @TempDir private Path temp;
 
   private static SparkSession spark = null;
 
-  private final Map<String, String> properties;
-
-  public TestSnapshotSelection(PlanningMode planningMode) {
-    this.properties =
-        ImmutableMap.of(
-            TableProperties.DATA_PLANNING_MODE, planningMode.modeName(),
-            TableProperties.DELETE_PLANNING_MODE, planningMode.modeName());
-  }
+  @Parameter(index = 0)
+  private Map<String, String> properties;
 
-  @BeforeClass
+  @BeforeAll
   public static void startSpark() {
     TestSnapshotSelection.spark = SparkSession.builder().master("local[2]").getOrCreate();
   }
 
-  @AfterClass
+  @AfterAll
   public static void stopSpark() {
     SparkSession currentSpark = TestSnapshotSelection.spark;
     TestSnapshotSelection.spark = null;
     currentSpark.stop();
   }
 
-  @Test
-  public void testSnapshotSelectionById() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionById() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -115,7 +118,7 @@ public class TestSnapshotSelection {
     Dataset<Row> secondDf = spark.createDataFrame(secondBatchRecords, SimpleRecord.class);
     secondDf.select("id", "data").write().format("iceberg").mode("append").save(tableLocation);
 
-    Assert.assertEquals("Expected 2 snapshots", 2, Iterables.size(table.snapshots()));
+    assertThat(table.snapshots()).as("Expected 2 snapshots").hasSize(2);
 
     // verify records in the current snapshot
     Dataset<Row> currentSnapshotResult = spark.read().format("iceberg").load(tableLocation);
@@ -124,8 +127,9 @@ public class TestSnapshotSelection {
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
     expectedRecords.addAll(secondBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, currentSnapshotRecords);
+    assertThat(currentSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
 
     // verify records in the previous snapshot
     Snapshot currentSnapshot = table.currentSnapshot();
@@ -134,13 +138,14 @@ public class TestSnapshotSelection {
         spark.read().format("iceberg").option("snapshot-id", parentSnapshotId).load(tableLocation);
     List<SimpleRecord> previousSnapshotRecords =
         previousSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals(
-        "Previous snapshot rows should match", firstBatchRecords, previousSnapshotRecords);
+    assertThat(previousSnapshotRecords)
+        .as("Previous snapshot rows should match")
+        .isEqualTo(firstBatchRecords);
   }
 
-  @Test
-  public void testSnapshotSelectionByTimestamp() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByTimestamp() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -163,7 +168,7 @@ public class TestSnapshotSelection {
     Dataset<Row> secondDf = spark.createDataFrame(secondBatchRecords, SimpleRecord.class);
     secondDf.select("id", "data").write().format("iceberg").mode("append").save(tableLocation);
 
-    Assert.assertEquals("Expected 2 snapshots", 2, Iterables.size(table.snapshots()));
+    assertThat(table.snapshots()).as("Expected 2 snapshots").hasSize(2);
 
     // verify records in the current snapshot
     Dataset<Row> currentSnapshotResult = spark.read().format("iceberg").load(tableLocation);
@@ -172,8 +177,9 @@ public class TestSnapshotSelection {
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
     expectedRecords.addAll(secondBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, currentSnapshotRecords);
+    assertThat(currentSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
 
     // verify records in the previous snapshot
     Dataset<Row> previousSnapshotResult =
@@ -184,13 +190,14 @@ public class TestSnapshotSelection {
             .load(tableLocation);
     List<SimpleRecord> previousSnapshotRecords =
         previousSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals(
-        "Previous snapshot rows should match", firstBatchRecords, previousSnapshotRecords);
+    assertThat(previousSnapshotRecords)
+        .as("Previous snapshot rows should match")
+        .isEqualTo(firstBatchRecords);
   }
 
-  @Test
-  public void testSnapshotSelectionByInvalidSnapshotId() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByInvalidSnapshotId() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -203,11 +210,11 @@ public class TestSnapshotSelection {
         .hasMessage("Cannot find snapshot with ID -10");
   }
 
-  @Test
-  public void testSnapshotSelectionByInvalidTimestamp() throws IOException {
+  @TestTemplate
+  public void testSnapshotSelectionByInvalidTimestamp() {
     long timestamp = System.currentTimeMillis();
 
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
     tables.create(SCHEMA, spec, properties, tableLocation);
@@ -223,9 +230,9 @@ public class TestSnapshotSelection {
         .hasMessageContaining("Cannot find a snapshot older than");
   }
 
-  @Test
-  public void testSnapshotSelectionBySnapshotIdAndTimestamp() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionBySnapshotIdAndTimestamp() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -255,9 +262,9 @@ public class TestSnapshotSelection {
         .hasMessageContaining("tag");
   }
 
-  @Test
-  public void testSnapshotSelectionByTag() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByTag() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -286,13 +293,14 @@ public class TestSnapshotSelection {
         currentSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, currentSnapshotRecords);
+    assertThat(currentSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
   }
 
-  @Test
-  public void testSnapshotSelectionByBranch() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByBranch() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -321,13 +329,14 @@ public class TestSnapshotSelection {
         currentSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, currentSnapshotRecords);
+    assertThat(currentSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
   }
 
-  @Test
-  public void testSnapshotSelectionByBranchAndTagFails() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByBranchAndTagFails() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -356,9 +365,9 @@ public class TestSnapshotSelection {
         .hasMessageStartingWith("Can specify only one of snapshot-id");
   }
 
-  @Test
-  public void testSnapshotSelectionByTimestampAndBranchOrTagFails() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByTimestampAndBranchOrTagFails() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -399,9 +408,9 @@ public class TestSnapshotSelection {
         .hasMessageStartingWith("Can specify only one of snapshot-id");
   }
 
-  @Test
-  public void testSnapshotSelectionByBranchWithSchemaChange() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByBranchWithSchemaChange() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -422,8 +431,9 @@ public class TestSnapshotSelection {
         branchSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, branchSnapshotRecords);
+    assertThat(branchSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
 
     // Deleting a column to indicate schema change
     table.updateSchema().deleteColumn("data").commit();
@@ -455,9 +465,9 @@ public class TestSnapshotSelection {
             new SimpleRecord(1, null), new SimpleRecord(2, null), new SimpleRecord(3, null));
   }
 
-  @Test
-  public void testWritingToBranchAfterSchemaChange() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testWritingToBranchAfterSchemaChange() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -478,8 +488,9 @@ public class TestSnapshotSelection {
         branchSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expectedRecords = Lists.newArrayList();
     expectedRecords.addAll(firstBatchRecords);
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, branchSnapshotRecords);
+    assertThat(branchSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
 
     // Deleting and add a new column of the same type to indicate schema change
     table.updateSchema().deleteColumn("data").addColumn("zip", Types.IntegerType.get()).commit();
@@ -528,9 +539,9 @@ public class TestSnapshotSelection {
         .containsAll(records);
   }
 
-  @Test
-  public void testSnapshotSelectionByTagWithSchemaChange() throws IOException {
-    String tableLocation = temp.newFolder("iceberg-table").toString();
+  @TestTemplate
+  public void testSnapshotSelectionByTagWithSchemaChange() {
+    String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
     PartitionSpec spec = PartitionSpec.unpartitioned();
@@ -552,7 +563,9 @@ public class TestSnapshotSelection {
         spark.read().format("iceberg").option("tag", "tag").load(tableLocation);
     List<SimpleRecord> tagSnapshotRecords =
         tagSnapshotResult.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Current snapshot rows should match", expectedRecords, tagSnapshotRecords);
+    assertThat(tagSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
 
     // Deleting a column to indicate schema change
     table.updateSchema().deleteColumn("data").commit();
@@ -565,7 +578,8 @@ public class TestSnapshotSelection {
             .orderBy("id")
             .as(Encoders.bean(SimpleRecord.class))
             .collectAsList();
-    Assert.assertEquals(
-        "Current snapshot rows should match", expectedRecords, deletedColumnTagSnapshotRecords);
+    assertThat(deletedColumnTagSnapshotRecords)
+        .as("Current snapshot rows should match")
+        .isEqualTo(expectedRecords);
   }
 }
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
index 63c18277aa..2cb1a52b69 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
@@ -22,12 +22,13 @@ import static org.apache.iceberg.TableProperties.SPARK_WRITE_PARTITIONED_FANOUT_
 import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assumptions.assumeThat;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
-import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import org.apache.hadoop.conf.Configuration;
@@ -36,6 +37,9 @@ import org.apache.iceberg.DataFile;
 import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.ManifestFile;
 import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.SnapshotRef;
@@ -54,64 +58,61 @@ import org.apache.spark.sql.Encoders;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.SaveMode;
 import org.apache.spark.sql.SparkSession;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+@ExtendWith(ParameterizedTestExtension.class)
 public class TestSparkDataWrite {
   private static final Configuration CONF = new Configuration();
-  private final FileFormat format;
-  private final String branch;
+
+  @Parameter(index = 0)
+  private FileFormat format;
+
+  @Parameter(index = 1)
+  private String branch;
+
   private static SparkSession spark = null;
   private static final Schema SCHEMA =
       new Schema(
           optional(1, "id", Types.IntegerType.get()), optional(2, "data", Types.StringType.get()));
 
-  @Rule public TemporaryFolder temp = new TemporaryFolder();
+  @TempDir private Path temp;
 
-  @Parameterized.Parameters(name = "format = {0}, branch = {1}")
-  public static Object[] parameters() {
-    return new Object[] {
-      new Object[] {"parquet", null},
-      new Object[] {"parquet", "main"},
-      new Object[] {"parquet", "testBranch"},
-      new Object[] {"avro", null},
-      new Object[] {"orc", "testBranch"}
+  @Parameters(name = "format = {0}, branch = {1}")
+  public static Object[][] parameters() {
+    return new Object[][] {
+      new Object[] {FileFormat.PARQUET, null},
+      new Object[] {FileFormat.PARQUET, "main"},
+      new Object[] {FileFormat.PARQUET, "testBranch"},
+      new Object[] {FileFormat.AVRO, null},
+      new Object[] {FileFormat.ORC, "testBranch"}
     };
   }
 
-  @BeforeClass
+  @BeforeAll
   public static void startSpark() {
     TestSparkDataWrite.spark = SparkSession.builder().master("local[2]").getOrCreate();
   }
 
-  @Parameterized.AfterParam
-  public static void clearSourceCache() {
+  @AfterEach
+  public void clearSourceCache() {
     ManualSource.clearTables();
   }
 
-  @AfterClass
+  @AfterAll
   public static void stopSpark() {
     SparkSession currentSpark = TestSparkDataWrite.spark;
     TestSparkDataWrite.spark = null;
     currentSpark.stop();
   }
 
-  public TestSparkDataWrite(String format, String branch) {
-    this.format = FileFormat.fromString(format);
-    this.branch = branch;
-  }
-
-  @Test
-  public void testBasicWrite() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testBasicWrite() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -139,31 +140,30 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
     for (ManifestFile manifest :
         SnapshotUtil.latestSnapshot(table, branch).allManifests(table.io())) {
       for (DataFile file : ManifestFiles.read(manifest, table.io())) {
         // TODO: avro not support split
         if (!format.equals(FileFormat.AVRO)) {
-          Assert.assertNotNull("Split offsets not present", file.splitOffsets());
+          assertThat(file.splitOffsets()).as("Split offsets not present").isNotNull();
         }
-        Assert.assertEquals("Should have reported record count as 1", 1, file.recordCount());
+        assertThat(file.recordCount()).as("Should have reported record count as 1").isEqualTo(1);
         // TODO: append more metric info
         if (format.equals(FileFormat.PARQUET)) {
-          Assert.assertNotNull("Column sizes metric not present", file.columnSizes());
-          Assert.assertNotNull("Counts metric not present", file.valueCounts());
-          Assert.assertNotNull("Null value counts metric not present", file.nullValueCounts());
-          Assert.assertNotNull("Lower bounds metric not present", file.lowerBounds());
-          Assert.assertNotNull("Upper bounds metric not present", file.upperBounds());
+          assertThat(file.columnSizes()).as("Column sizes metric not present").isNotNull();
+          assertThat(file.valueCounts()).as("Counts metric not present").isNotNull();
+          assertThat(file.nullValueCounts()).as("Null value counts metric not present").isNotNull();
+          assertThat(file.lowerBounds()).as("Lower bounds metric not present").isNotNull();
+          assertThat(file.upperBounds()).as("Upper bounds metric not present").isNotNull();
         }
       }
     }
   }
 
-  @Test
-  public void testAppend() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testAppend() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -209,13 +209,12 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testEmptyOverwrite() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testEmptyOverwrite() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -255,13 +254,12 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testOverwrite() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testOverwrite() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -308,13 +306,12 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testUnpartitionedOverwrite() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testUnpartitionedOverwrite() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -351,13 +348,12 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testUnpartitionedCreateWithTargetFileSizeViaTableProperties() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testUnpartitionedCreateWithTargetFileSizeViaTableProperties() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -391,8 +387,7 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
 
     List<DataFile> files = Lists.newArrayList();
     for (ManifestFile manifest :
@@ -402,33 +397,37 @@ public class TestSparkDataWrite {
       }
     }
 
-    Assert.assertEquals("Should have 4 DataFiles", 4, files.size());
-    Assert.assertTrue(
-        "All DataFiles contain 1000 rows", files.stream().allMatch(d -> d.recordCount() == 1000));
+    assertThat(files)
+        .hasSize(4)
+        .allSatisfy(
+            dataFile ->
+                assertThat(dataFile.recordCount())
+                    .as("All DataFiles contain 1000 rows")
+                    .isEqualTo(1000));
   }
 
-  @Test
-  public void testPartitionedCreateWithTargetFileSizeViaOption() throws IOException {
+  @TestTemplate
+  public void testPartitionedCreateWithTargetFileSizeViaOption() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.NONE);
   }
 
-  @Test
-  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption() throws IOException {
+  @TestTemplate
+  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.TABLE);
   }
 
-  @Test
-  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption2() throws IOException {
+  @TestTemplate
+  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption2() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.JOB);
   }
 
-  @Test
-  public void testWriteProjection() throws IOException {
-    Assume.assumeTrue(
-        "Not supported in Spark 3; analysis requires all columns are present",
-        spark.version().startsWith("2"));
+  @TestTemplate
+  public void testWriteProjection() {
+    assumeThat(spark.version())
+        .as("Not supported in Spark 3; analysis requires all columns are present")
+        .startsWith("2");
 
-    File parent = temp.newFolder(format.toString());
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -456,17 +455,16 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testWriteProjectionWithMiddle() throws IOException {
-    Assume.assumeTrue(
-        "Not supported in Spark 3; analysis requires all columns are present",
-        spark.version().startsWith("2"));
+  @TestTemplate
+  public void testWriteProjectionWithMiddle() {
+    assumeThat(spark.version())
+        .as("Not supported in Spark 3; analysis requires all columns are present")
+        .startsWith("2");
 
-    File parent = temp.newFolder(format.toString());
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -501,13 +499,12 @@ public class TestSparkDataWrite {
 
     List<ThreeColumnRecord> actual =
         result.orderBy("c1").as(Encoders.bean(ThreeColumnRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
-  @Test
-  public void testViewsReturnRecentResults() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testViewsReturnRecentResults() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -537,8 +534,7 @@ public class TestSparkDataWrite {
     List<SimpleRecord> actual1 =
         spark.table("tmp").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expected1 = Lists.newArrayList(new SimpleRecord(1, "a"));
-    Assert.assertEquals("Number of rows should match", expected1.size(), actual1.size());
-    Assert.assertEquals("Result rows should match", expected1, actual1);
+    assertThat(actual1).hasSameSizeAs(expected1).isEqualTo(expected1);
 
     df.select("id", "data")
         .write()
@@ -551,13 +547,11 @@ public class TestSparkDataWrite {
         spark.table("tmp").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expected2 =
         Lists.newArrayList(new SimpleRecord(1, "a"), new SimpleRecord(1, "a"));
-    Assert.assertEquals("Number of rows should match", expected2.size(), actual2.size());
-    Assert.assertEquals("Result rows should match", expected2, actual2);
+    assertThat(actual2).hasSameSizeAs(expected2).isEqualTo(expected2);
   }
 
-  public void partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType option)
-      throws IOException {
-    File parent = temp.newFolder(format.toString());
+  public void partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType option) {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
 
@@ -620,8 +614,7 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals("Number of rows should match", expected.size(), actual.size());
-    Assert.assertEquals("Result rows should match", expected, actual);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
 
     List<DataFile> files = Lists.newArrayList();
     for (ManifestFile manifest :
@@ -631,14 +624,18 @@ public class TestSparkDataWrite {
       }
     }
 
-    Assert.assertEquals("Should have 8 DataFiles", 8, files.size());
-    Assert.assertTrue(
-        "All DataFiles contain 1000 rows", files.stream().allMatch(d -> d.recordCount() == 1000));
+    assertThat(files)
+        .hasSize(8)
+        .allSatisfy(
+            dataFile ->
+                assertThat(dataFile.recordCount())
+                    .as("All DataFiles contain 1000 rows")
+                    .isEqualTo(1000));
   }
 
-  @Test
-  public void testCommitUnknownException() throws IOException {
-    File parent = temp.newFolder(format.toString());
+  @TestTemplate
+  public void testCommitUnknownException() {
+    File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "commitunknown");
     String targetLocation = locationWithBranch(location);
 
@@ -706,10 +703,8 @@ public class TestSparkDataWrite {
     Dataset<Row> result = spark.read().format("iceberg").load(targetLocation);
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    Assert.assertEquals(
-        "Number of rows should match", records.size() + records2.size(), actual.size());
     assertThat(actual)
-        .describedAs("Result rows should match")
+        .hasSize(records.size() + records2.size())
         .containsExactlyInAnyOrder(
             ImmutableList.<SimpleRecord>builder()
                 .addAll(records)
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReadProjection.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReadProjection.java
index 584a6b1c70..becf6a064d 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReadProjection.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReadProjection.java
@@ -23,6 +23,7 @@ import static org.apache.iceberg.PlanningMode.DISTRIBUTED;
 import static org.apache.iceberg.PlanningMode.LOCAL;
 import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.apache.iceberg.types.Types.NestedField.required;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.File;
 import java.io.IOException;
@@ -32,6 +33,9 @@ import java.util.UUID;
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.Parameter;
+import org.apache.iceberg.ParameterizedTestExtension;
+import org.apache.iceberg.Parameters;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.PlanningMode;
 import org.apache.iceberg.Schema;
@@ -51,40 +55,33 @@ import org.apache.iceberg.types.Types;
 import org.apache.spark.sql.Dataset;
 import org.apache.spark.sql.Row;
 import org.apache.spark.sql.SparkSession;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.extension.ExtendWith;
 
-@RunWith(Parameterized.class)
+@ExtendWith(ParameterizedTestExtension.class)
 public class TestSparkReadProjection extends TestReadProjection {
 
   private static SparkSession spark = null;
 
-  @Parameterized.Parameters(name = "format = {0}, vectorized = {1}, planningMode = {2}")
+  @Parameters(name = "format = {0}, vectorized = {1}, planningMode = {2}")
   public static Object[][] parameters() {
     return new Object[][] {
-      {"parquet", false, LOCAL},
-      {"parquet", true, DISTRIBUTED},
-      {"avro", false, LOCAL},
-      {"orc", false, DISTRIBUTED},
-      {"orc", true, LOCAL}
+      {FileFormat.PARQUET, false, LOCAL},
+      {FileFormat.PARQUET, true, DISTRIBUTED},
+      {FileFormat.AVRO, false, LOCAL},
+      {FileFormat.ORC, false, DISTRIBUTED},
+      {FileFormat.ORC, true, LOCAL}
     };
   }
 
-  private final FileFormat format;
-  private final boolean vectorized;
-  private final PlanningMode planningMode;
+  @Parameter(index = 1)
+  private boolean vectorized;
 
-  public TestSparkReadProjection(String format, boolean vectorized, PlanningMode planningMode) {
-    super(format);
-    this.format = FileFormat.fromString(format);
-    this.vectorized = vectorized;
-    this.planningMode = planningMode;
-  }
+  @Parameter(index = 2)
+  private PlanningMode planningMode;
 
-  @BeforeClass
+  @BeforeAll
   public static void startSpark() {
     TestSparkReadProjection.spark = SparkSession.builder().master("local[2]").getOrCreate();
     ImmutableMap<String, String> config =
@@ -100,7 +97,7 @@ public class TestSparkReadProjection extends TestReadProjection {
         (key, value) -> spark.conf().set("spark.sql.catalog.spark_catalog." + key, value));
   }
 
-  @AfterClass
+  @AfterAll
   public static void stopSpark() {
     SparkSession currentSpark = TestSparkReadProjection.spark;
     TestSparkReadProjection.spark = null;
@@ -110,10 +107,10 @@ public class TestSparkReadProjection extends TestReadProjection {
   @Override
   protected Record writeAndRead(String desc, Schema writeSchema, Schema readSchema, Record record)
       throws IOException {
-    File parent = temp.newFolder(desc);
+    File parent = new File(temp.toFile(), desc);
     File location = new File(parent, "test");
     File dataFolder = new File(location, "data");
-    Assert.assertTrue("mkdirs should succeed", dataFolder.mkdirs());
+    assertThat(dataFolder.mkdirs()).as("mkdirs should succeed").isTrue();
 
     File testFile = new File(dataFolder, format.addExtension(UUID.randomUUID().toString()));
 
diff --git a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkTable.java b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkTable.java
index 4a386ee861..d14b1a52cf 100644
--- a/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkTable.java
+++ b/spark/v3.4/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkTable.java
@@ -18,13 +18,14 @@
  */
 package org.apache.iceberg.spark.source;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 import org.apache.iceberg.ParameterizedTestExtension;
 import org.apache.iceberg.spark.CatalogTestBase;
 import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
 import org.apache.spark.sql.connector.catalog.CatalogManager;
 import org.apache.spark.sql.connector.catalog.Identifier;
 import org.apache.spark.sql.connector.catalog.TableCatalog;
-import org.junit.Assert;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestTemplate;
@@ -52,7 +53,7 @@ public class TestSparkTable extends CatalogTestBase {
     SparkTable table2 = (SparkTable) catalog.loadTable(identifier);
 
     // different instances pointing to the same table must be equivalent
-    Assert.assertNotSame("References must be different", table1, table2);
-    Assert.assertEquals("Tables must be equivalent", table1, table2);
+    assertThat(table1).as("References must be different").isNotSameAs(table2);
+    assertThat(table1).as("Tables must be equivalent").isEqualTo(table2);
   }
 }
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
index e1b75ca55e..9fc71125a9 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/SparkTestHelperBase.java
@@ -57,7 +57,7 @@ public class SparkTestHelperBase {
   protected void assertEquals(
       String context, List<Object[]> expectedRows, List<Object[]> actualRows) {
     assertThat(actualRows)
-        .as(context + ": number of results should match")
+        .as("%s: number of results should match", context)
         .hasSameSizeAs(expectedRows);
     for (int row = 0; row < expectedRows.size(); row += 1) {
       Object[] expected = expectedRows.get(row);
@@ -80,9 +80,7 @@ public class SparkTestHelperBase {
           assertEquals(newContext, (Object[]) expectedValue, (Object[]) actualValue);
         }
       } else if (expectedValue != ANY) {
-        assertThat(actualValue)
-            .as(context + " col " + (col + 1) + " contents should match")
-            .isEqualTo(expectedValue);
+        assertThat(actualValue).as("%s contents should match", context).isEqualTo(expectedValue);
       }
     }
   }
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
index a6205ae9ea..49c38b34d3 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkFilters.java
@@ -60,61 +60,68 @@ public class TestSparkFilters {
           IsNull isNull = IsNull.apply(quoted);
           Expression expectedIsNull = Expressions.isNull(unquoted);
           Expression actualIsNull = SparkFilters.convert(isNull);
-          assertThat(actualIsNull.toString())
+          assertThat(actualIsNull)
+              .asString()
               .as("IsNull must match")
               .isEqualTo(expectedIsNull.toString());
 
           IsNotNull isNotNull = IsNotNull.apply(quoted);
           Expression expectedIsNotNull = Expressions.notNull(unquoted);
           Expression actualIsNotNull = SparkFilters.convert(isNotNull);
-          assertThat(actualIsNotNull.toString())
+          assertThat(actualIsNotNull)
+              .asString()
               .as("IsNotNull must match")
               .isEqualTo(expectedIsNotNull.toString());
 
           LessThan lt = LessThan.apply(quoted, 1);
           Expression expectedLt = Expressions.lessThan(unquoted, 1);
           Expression actualLt = SparkFilters.convert(lt);
-          assertThat(actualLt.toString())
+          assertThat(actualLt)
+              .asString()
               .as("LessThan must match")
               .isEqualTo(expectedLt.toString());
 
           LessThanOrEqual ltEq = LessThanOrEqual.apply(quoted, 1);
           Expression expectedLtEq = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualLtEq = SparkFilters.convert(ltEq);
-          assertThat(actualLtEq.toString())
+          assertThat(actualLtEq)
+              .asString()
               .as("LessThanOrEqual must match")
               .isEqualTo(expectedLtEq.toString());
 
           GreaterThan gt = GreaterThan.apply(quoted, 1);
           Expression expectedGt = Expressions.greaterThan(unquoted, 1);
           Expression actualGt = SparkFilters.convert(gt);
-          assertThat(actualGt.toString())
+          assertThat(actualGt)
+              .asString()
               .as("GreaterThan must match")
               .isEqualTo(expectedGt.toString());
 
           GreaterThanOrEqual gtEq = GreaterThanOrEqual.apply(quoted, 1);
           Expression expectedGtEq = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualGtEq = SparkFilters.convert(gtEq);
-          assertThat(actualGtEq.toString())
+          assertThat(actualGtEq)
+              .asString()
               .as("GreaterThanOrEqual must match")
               .isEqualTo(expectedGtEq.toString());
 
           EqualTo eq = EqualTo.apply(quoted, 1);
           Expression expectedEq = Expressions.equal(unquoted, 1);
           Expression actualEq = SparkFilters.convert(eq);
-          assertThat(actualEq.toString()).as("EqualTo must match").isEqualTo(expectedEq.toString());
+          assertThat(actualEq).asString().as("EqualTo must match").isEqualTo(expectedEq.toString());
 
           EqualNullSafe eqNullSafe = EqualNullSafe.apply(quoted, 1);
           Expression expectedEqNullSafe = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe = SparkFilters.convert(eqNullSafe);
-          assertThat(actualEqNullSafe.toString())
+          assertThat(actualEqNullSafe)
+              .asString()
               .as("EqualNullSafe must match")
               .isEqualTo(expectedEqNullSafe.toString());
 
           In in = In.apply(quoted, new Integer[] {1});
           Expression expectedIn = Expressions.in(unquoted, 1);
           Expression actualIn = SparkFilters.convert(in);
-          assertThat(actualIn.toString()).as("In must match").isEqualTo(expectedIn.toString());
+          assertThat(actualIn).asString().as("In must match").isEqualTo(expectedIn.toString());
         });
   }
 
@@ -128,11 +135,13 @@ public class TestSparkFilters {
     Expression timestampExpression = SparkFilters.convert(GreaterThan.apply("x", timestamp));
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    assertThat(timestampExpression.toString())
+    assertThat(timestampExpression)
+        .asString()
         .as("Generated Timestamp expression should be correct")
         .isEqualTo(rawExpression.toString());
 
-    assertThat(instantExpression.toString())
+    assertThat(instantExpression)
+        .asString()
         .as("Generated Instant expression should be correct")
         .isEqualTo(rawExpression.toString());
   }
@@ -146,7 +155,8 @@ public class TestSparkFilters {
     Expression instantExpression = SparkFilters.convert(GreaterThan.apply("x", ldt));
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    assertThat(instantExpression.toString())
+    assertThat(instantExpression)
+        .asString()
         .as("Generated Instant expression should be correct")
         .isEqualTo(rawExpression.toString());
   }
@@ -161,11 +171,13 @@ public class TestSparkFilters {
     Expression dateExpression = SparkFilters.convert(GreaterThan.apply("x", date));
     Expression rawExpression = Expressions.greaterThan("x", epochDay);
 
-    assertThat(localDateExpression.toString())
+    assertThat(localDateExpression)
+        .asString()
         .as("Generated localdate expression should be correct")
         .isEqualTo(rawExpression.toString());
 
-    assertThat(dateExpression.toString())
+    assertThat(dateExpression)
+        .asString()
         .as("Generated date expression should be correct")
         .isEqualTo(rawExpression.toString());
   }
@@ -184,6 +196,6 @@ public class TestSparkFilters {
     Expression actual = SparkFilters.convert(filter);
     Expression expected =
         Expressions.and(Expressions.notNull("col"), Expressions.notIn("col", 1, 2));
-    assertThat(actual.toString()).as("Expressions should match").isEqualTo(expected.toString());
+    assertThat(actual).asString().as("Expressions should match").isEqualTo(expected.toString());
   }
 }
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
index 4d4091bf9a..4045847d5a 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkSchemaUtil.java
@@ -21,7 +21,6 @@ package org.apache.iceberg.spark;
 import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.assertj.core.api.Assertions.assertThat;
 
-import java.io.IOException;
 import java.util.List;
 import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.Schema;
@@ -45,21 +44,21 @@ public class TestSparkSchemaUtil {
           MetadataColumns.ROW_POSITION);
 
   @Test
-  public void testEstimateSizeMaxValue() throws IOException {
+  public void testEstimateSizeMaxValue() {
     assertThat(SparkSchemaUtil.estimateSize(null, Long.MAX_VALUE))
         .as("estimateSize returns Long max value")
         .isEqualTo(Long.MAX_VALUE);
   }
 
   @Test
-  public void testEstimateSizeWithOverflow() throws IOException {
+  public void testEstimateSizeWithOverflow() {
     long tableSize =
         SparkSchemaUtil.estimateSize(SparkSchemaUtil.convert(TEST_SCHEMA), Long.MAX_VALUE - 1);
     assertThat(tableSize).as("estimateSize handles overflow").isEqualTo(Long.MAX_VALUE);
   }
 
   @Test
-  public void testEstimateSize() throws IOException {
+  public void testEstimateSize() {
     long tableSize = SparkSchemaUtil.estimateSize(SparkSchemaUtil.convert(TEST_SCHEMA), 1);
     assertThat(tableSize).as("estimateSize matches with expected approximation").isEqualTo(24);
   }
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
index 772ae3a224..93e4c89687 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkTableUtil.java
@@ -68,11 +68,14 @@ public class TestSparkTableUtil {
     MetricsConfig config = MetricsConfig.fromProperties(metricsConfig);
     MetricsConfig deserialized = KryoHelpers.roundTripSerialize(config);
 
-    assertThat(deserialized.columnMode("col1").toString())
+    assertThat(deserialized.columnMode("col1"))
+        .asString()
         .isEqualTo(MetricsModes.Full.get().toString());
-    assertThat(deserialized.columnMode("col2").toString())
+    assertThat(deserialized.columnMode("col2"))
+        .asString()
         .isEqualTo(MetricsModes.Truncate.withLength(16).toString());
-    assertThat(deserialized.columnMode("col3").toString())
+    assertThat(deserialized.columnMode("col3"))
+        .asString()
         .isEqualTo(MetricsModes.Counts.get().toString());
   }
 
@@ -90,11 +93,14 @@ public class TestSparkTableUtil {
     MetricsConfig config = MetricsConfig.fromProperties(metricsConfig);
     MetricsConfig deserialized = TestHelpers.roundTripSerialize(config);
 
-    assertThat(deserialized.columnMode("col1").toString())
+    assertThat(deserialized.columnMode("col1"))
+        .asString()
         .isEqualTo(MetricsModes.Full.get().toString());
-    assertThat(deserialized.columnMode("col2").toString())
+    assertThat(deserialized.columnMode("col2"))
+        .asString()
         .isEqualTo(MetricsModes.Truncate.withLength(16).toString());
-    assertThat(deserialized.columnMode("col3").toString())
+    assertThat(deserialized.columnMode("col3"))
+        .asString()
         .isEqualTo(MetricsModes.Counts.get().toString());
   }
 }
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
index 44fb64120c..e0b590e5a6 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/TestSparkV2Filters.java
@@ -89,112 +89,128 @@ public class TestSparkV2Filters {
           Predicate isNull = new Predicate("IS_NULL", attrOnly);
           Expression expectedIsNull = Expressions.isNull(unquoted);
           Expression actualIsNull = SparkV2Filters.convert(isNull);
-          assertThat(actualIsNull.toString())
+          assertThat(actualIsNull)
+              .asString()
               .as("IsNull must match")
               .isEqualTo(expectedIsNull.toString());
 
           Predicate isNotNull = new Predicate("IS_NOT_NULL", attrOnly);
           Expression expectedIsNotNull = Expressions.notNull(unquoted);
           Expression actualIsNotNull = SparkV2Filters.convert(isNotNull);
-          assertThat(actualIsNotNull.toString())
+          assertThat(actualIsNotNull)
+              .asString()
               .as("IsNotNull must match")
               .isEqualTo(expectedIsNotNull.toString());
 
           Predicate lt1 = new Predicate("<", attrAndValue);
           Expression expectedLt1 = Expressions.lessThan(unquoted, 1);
           Expression actualLt1 = SparkV2Filters.convert(lt1);
-          assertThat(actualLt1.toString())
+          assertThat(actualLt1)
+              .asString()
               .as("LessThan must match")
               .isEqualTo(expectedLt1.toString());
 
           Predicate lt2 = new Predicate("<", valueAndAttr);
           Expression expectedLt2 = Expressions.greaterThan(unquoted, 1);
           Expression actualLt2 = SparkV2Filters.convert(lt2);
-          assertThat(actualLt2.toString())
+          assertThat(actualLt2)
+              .asString()
               .as("LessThan must match")
               .isEqualTo(expectedLt2.toString());
 
           Predicate ltEq1 = new Predicate("<=", attrAndValue);
           Expression expectedLtEq1 = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualLtEq1 = SparkV2Filters.convert(ltEq1);
-          assertThat(actualLtEq1.toString())
+          assertThat(actualLtEq1)
+              .asString()
               .as("LessThanOrEqual must match")
               .isEqualTo(expectedLtEq1.toString());
 
           Predicate ltEq2 = new Predicate("<=", valueAndAttr);
           Expression expectedLtEq2 = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualLtEq2 = SparkV2Filters.convert(ltEq2);
-          assertThat(actualLtEq2.toString())
+          assertThat(actualLtEq2)
+              .asString()
               .as("LessThanOrEqual must match")
               .isEqualTo(expectedLtEq2.toString());
 
           Predicate gt1 = new Predicate(">", attrAndValue);
           Expression expectedGt1 = Expressions.greaterThan(unquoted, 1);
           Expression actualGt1 = SparkV2Filters.convert(gt1);
-          assertThat(actualGt1.toString())
+          assertThat(actualGt1)
+              .asString()
               .as("GreaterThan must match")
               .isEqualTo(expectedGt1.toString());
 
           Predicate gt2 = new Predicate(">", valueAndAttr);
           Expression expectedGt2 = Expressions.lessThan(unquoted, 1);
           Expression actualGt2 = SparkV2Filters.convert(gt2);
-          assertThat(actualGt2.toString())
+          assertThat(actualGt2)
+              .asString()
               .as("GreaterThan must match")
               .isEqualTo(expectedGt2.toString());
 
           Predicate gtEq1 = new Predicate(">=", attrAndValue);
           Expression expectedGtEq1 = Expressions.greaterThanOrEqual(unquoted, 1);
           Expression actualGtEq1 = SparkV2Filters.convert(gtEq1);
-          assertThat(actualGtEq1.toString())
+          assertThat(actualGtEq1)
+              .asString()
               .as("GreaterThanOrEqual must match")
               .isEqualTo(expectedGtEq1.toString());
 
           Predicate gtEq2 = new Predicate(">=", valueAndAttr);
           Expression expectedGtEq2 = Expressions.lessThanOrEqual(unquoted, 1);
           Expression actualGtEq2 = SparkV2Filters.convert(gtEq2);
-          assertThat(actualGtEq2.toString())
+          assertThat(actualGtEq2)
+              .asString()
               .as("GreaterThanOrEqual must match")
               .isEqualTo(expectedGtEq2.toString());
 
           Predicate eq1 = new Predicate("=", attrAndValue);
           Expression expectedEq1 = Expressions.equal(unquoted, 1);
           Expression actualEq1 = SparkV2Filters.convert(eq1);
-          assertThat(actualEq1.toString())
+          assertThat(actualEq1)
+              .asString()
               .as("EqualTo must match")
               .isEqualTo(expectedEq1.toString());
 
           Predicate eq2 = new Predicate("=", valueAndAttr);
           Expression expectedEq2 = Expressions.equal(unquoted, 1);
           Expression actualEq2 = SparkV2Filters.convert(eq2);
-          assertThat(actualEq2.toString())
+          assertThat(actualEq2)
+              .asString()
               .as("EqualTo must match")
               .isEqualTo(expectedEq2.toString());
 
           Predicate notEq1 = new Predicate("<>", attrAndValue);
           Expression expectedNotEq1 = Expressions.notEqual(unquoted, 1);
           Expression actualNotEq1 = SparkV2Filters.convert(notEq1);
-          assertThat(actualNotEq1.toString())
+          assertThat(actualNotEq1)
+              .asString()
               .as("NotEqualTo must match")
               .isEqualTo(expectedNotEq1.toString());
 
           Predicate notEq2 = new Predicate("<>", valueAndAttr);
           Expression expectedNotEq2 = Expressions.notEqual(unquoted, 1);
           Expression actualNotEq2 = SparkV2Filters.convert(notEq2);
-          assertThat(actualNotEq2.toString())
+          assertThat(actualNotEq2)
+              .asString()
               .as("NotEqualTo must match")
               .isEqualTo(expectedNotEq2.toString());
 
           Predicate eqNullSafe1 = new Predicate("<=>", attrAndValue);
           Expression expectedEqNullSafe1 = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe1 = SparkV2Filters.convert(eqNullSafe1);
-          assertThat(actualEqNullSafe1.toString())
+          assertThat(actualEqNullSafe1)
+              .asString()
               .as("EqualNullSafe must match")
               .isEqualTo(expectedEqNullSafe1.toString());
 
           Predicate eqNullSafe2 = new Predicate("<=>", valueAndAttr);
           Expression expectedEqNullSafe2 = Expressions.equal(unquoted, 1);
           Expression actualEqNullSafe2 = SparkV2Filters.convert(eqNullSafe2);
-          assertThat(actualEqNullSafe2.toString())
+          assertThat(actualEqNullSafe2)
+              .asString()
               .as("EqualNullSafe must match")
               .isEqualTo(expectedEqNullSafe2.toString());
 
@@ -205,19 +221,20 @@ public class TestSparkV2Filters {
           Predicate startsWith = new Predicate("STARTS_WITH", attrAndStr);
           Expression expectedStartsWith = Expressions.startsWith(unquoted, "iceberg");
           Expression actualStartsWith = SparkV2Filters.convert(startsWith);
-          assertThat(actualStartsWith.toString())
+          assertThat(actualStartsWith)
+              .asString()
               .as("StartsWith must match")
               .isEqualTo(expectedStartsWith.toString());
 
           Predicate in = new Predicate("IN", attrAndValue);
           Expression expectedIn = Expressions.in(unquoted, 1);
           Expression actualIn = SparkV2Filters.convert(in);
-          assertThat(actualIn.toString()).as("In must match").isEqualTo(expectedIn.toString());
+          assertThat(actualIn).asString().as("In must match").isEqualTo(expectedIn.toString());
 
           Predicate and = new And(lt1, eq1);
           Expression expectedAnd = Expressions.and(expectedLt1, expectedEq1);
           Expression actualAnd = SparkV2Filters.convert(and);
-          assertThat(actualAnd.toString()).as("And must match").isEqualTo(expectedAnd.toString());
+          assertThat(actualAnd).asString().as("And must match").isEqualTo(expectedAnd.toString());
 
           org.apache.spark.sql.connector.expressions.Expression[] attrAndAttr =
               new org.apache.spark.sql.connector.expressions.Expression[] {
@@ -231,7 +248,7 @@ public class TestSparkV2Filters {
           Predicate or = new Or(lt1, eq1);
           Expression expectedOr = Expressions.or(expectedLt1, expectedEq1);
           Expression actualOr = SparkV2Filters.convert(or);
-          assertThat(actualOr.toString()).as("Or must match").isEqualTo(expectedOr.toString());
+          assertThat(actualOr).asString().as("Or must match").isEqualTo(expectedOr.toString());
 
           Predicate orWithInvalidLeft = new Or(invalid, eq1);
           Expression convertedOr = SparkV2Filters.convert(orWithInvalidLeft);
@@ -240,7 +257,7 @@ public class TestSparkV2Filters {
           Predicate not = new Not(lt1);
           Expression expectedNot = Expressions.not(expectedLt1);
           Expression actualNot = SparkV2Filters.convert(not);
-          assertThat(actualNot.toString()).as("Not must match").isEqualTo(expectedNot.toString());
+          assertThat(actualNot).asString().as("Not must match").isEqualTo(expectedNot.toString());
         });
   }
 
@@ -396,7 +413,8 @@ public class TestSparkV2Filters {
     Expression tsExpression = SparkV2Filters.convert(predicate);
     Expression rawExpression = Expressions.greaterThan("x", epochMicros);
 
-    assertThat(tsExpression.toString())
+    assertThat(tsExpression)
+        .asString()
         .as("Generated Timestamp expression should be correct")
         .isEqualTo(rawExpression.toString());
   }
@@ -415,7 +433,8 @@ public class TestSparkV2Filters {
     Expression dateExpression = SparkV2Filters.convert(predicate);
     Expression rawExpression = Expressions.greaterThan("x", epochDay);
 
-    assertThat(dateExpression.toString())
+    assertThat(dateExpression)
+        .asString()
         .as("Generated date expression should be correct")
         .isEqualTo(rawExpression.toString());
   }
@@ -453,7 +472,7 @@ public class TestSparkV2Filters {
     Expression actual = SparkV2Filters.convert(not);
     Expression expected =
         Expressions.and(Expressions.notNull("col"), Expressions.notIn("col", 1, 2));
-    assertThat(actual.toString()).as("Expressions should match").isEqualTo(expected.toString());
+    assertThat(actual).asString().as("Expressions should match").isEqualTo(expected.toString());
   }
 
   @Test
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
index 7dbe99d64e..9cf8e43527 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestReadProjection.java
@@ -37,7 +37,6 @@ import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
-import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.types.Comparators;
 import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.TestTemplate;
@@ -73,7 +72,6 @@ public abstract class TestReadProjection {
 
     int cmp =
         Comparators.charSequences().compare("test", (CharSequence) projected.getField("data"));
-
     assertThat(cmp).as("Should contain the correct data value").isEqualTo(0);
   }
 
@@ -99,7 +97,8 @@ public abstract class TestReadProjection {
 
     Record projected = writeAndRead("reordered_full_projection", schema, reordered, record);
 
-    assertThat(projected.get(0).toString())
+    assertThat(projected.get(0))
+        .asString()
         .as("Should contain the correct 0 value")
         .isEqualTo("test");
     assertThat(projected.get(1)).as("Should contain the correct 1 value").isEqualTo(34L);
@@ -387,10 +386,7 @@ public abstract class TestReadProjection {
     assertThat(projected.getField("id")).as("Should not project id").isNull();
 
     Map<String, ?> locations = toStringMap((Map) projected.getField("locations"));
-    assertThat(locations).as("Should project locations map").isNotNull();
-    assertThat(locations.keySet())
-        .as("Should contain L1 and L2")
-        .isEqualTo(Sets.newHashSet("L1", "L2"));
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
 
     Record projectedL1 = (Record) locations.get("L1");
     assertThat(projectedL1).as("L1 should not be null").isNotNull();
@@ -411,10 +407,7 @@ public abstract class TestReadProjection {
     assertThat(projected.getField("id")).as("Should not project id").isNull();
 
     locations = toStringMap((Map) projected.getField("locations"));
-    assertThat(locations).as("Should project locations map").isNotNull();
-    assertThat(locations.keySet())
-        .as("Should contain L1 and L2")
-        .isEqualTo(Sets.newHashSet("L1", "L2"));
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
 
     projectedL1 = (Record) locations.get("L1");
     assertThat(projectedL1).as("L1 should not be null").isNotNull();
@@ -445,10 +438,7 @@ public abstract class TestReadProjection {
     projected = writeAndRead("latitude_renamed", writeSchema, latitiudeRenamed, record);
     assertThat(projected.getField("id")).as("Should not project id").isNull();
     locations = toStringMap((Map) projected.getField("locations"));
-    assertThat(locations).as("Should project locations map").isNotNull();
-    assertThat(locations.keySet())
-        .as("Should contain L1 and L2")
-        .isEqualTo(Sets.newHashSet("L1", "L2"));
+    assertThat(locations).isNotNull().containsKeys("L1", "L2");
 
     projectedL1 = (Record) locations.get("L1");
     assertThat(projectedL1).as("L1 should not be null").isNotNull();
@@ -545,14 +535,20 @@ public abstract class TestReadProjection {
 
     List<Record> points = (List<Record>) projected.getField("points");
     assertThat(points).as("Should read 2 points").hasSize(2);
-
-    Record projectedP1 = points.get(0);
-    assertThat((int) projectedP1.getField("x")).as("Should project x").isEqualTo(1);
-    assertThat(projected.getField("y")).as("Should not project y").isNull();
-
-    Record projectedP2 = points.get(1);
-    assertThat((int) projectedP2.getField("x")).as("Should project x").isEqualTo(3);
-    assertThat(projected.getField("y")).as("Should not project y").isNull();
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat((int) projectedP1.getField("x")).isEqualTo(1);
+              assertThat(projectedP1.getField("y")).isNull();
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat((int) projectedP2.getField("x")).isEqualTo(3);
+              assertThat(projectedP2.getField("y")).isNull();
+            });
 
     projected = writeAndRead("y_only", writeSchema, writeSchema.select("points.y"), record);
     assertThat(projected.getField("id")).as("Should not project id").isNull();
@@ -560,14 +556,20 @@ public abstract class TestReadProjection {
 
     points = (List<Record>) projected.getField("points");
     assertThat(points).as("Should read 2 points").hasSize(2);
-
-    projectedP1 = points.get(0);
-    assertThat(projectedP1.getField("x")).as("Should not project x").isNull();
-    assertThat((int) projectedP1.getField("y")).as("Should project y").isEqualTo(2);
-
-    projectedP2 = points.get(1);
-    assertThat(projectedP2.getField("x")).as("Should not project x").isNull();
-    assertThat(projectedP2.getField("y")).as("Should not project y").isNull();
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat(projectedP1.getField("x")).isNull();
+              assertThat((int) projectedP1.getField("y")).isEqualTo(2);
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat(projectedP2.getField("x")).isNull();
+              assertThat(projectedP2.getField("y")).isNull();
+            });
 
     Schema yRenamed =
         new Schema(
@@ -585,16 +587,22 @@ public abstract class TestReadProjection {
 
     points = (List<Record>) projected.getField("points");
     assertThat(points).as("Should read 2 points").hasSize(2);
-
-    projectedP1 = points.get(0);
-    assertThat(projectedP1.getField("x")).as("Should not project x").isNull();
-    assertThat(projectedP1.getField("y")).as("Should not project y").isNull();
-    assertThat((int) projectedP1.getField("z")).as("Should project z").isEqualTo(2);
-
-    projectedP2 = points.get(1);
-    assertThat(projectedP2.getField("x")).as("Should not project x").isNull();
-    assertThat(projectedP2.getField("y")).as("Should not project y").isNull();
-    assertThat(projectedP2.getField("z")).as("Should project null z").isNull();
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat(projectedP1.getField("x")).isNull();
+              assertThat(projectedP1.getField("y")).isNull();
+              assertThat((int) projectedP1.getField("z")).isEqualTo(2);
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat(projectedP2.getField("x")).isNull();
+              assertThat(projectedP2.getField("y")).isNull();
+              assertThat(projectedP2.getField("z")).isNull();
+            });
 
     Schema zAdded =
         new Schema(
@@ -614,16 +622,22 @@ public abstract class TestReadProjection {
 
     points = (List<Record>) projected.getField("points");
     assertThat(points).as("Should read 2 points").hasSize(2);
-
-    projectedP1 = points.get(0);
-    assertThat((int) projectedP1.getField("x")).as("Should project x").isEqualTo(1);
-    assertThat((int) projectedP1.getField("y")).as("Should project y").isEqualTo(2);
-    assertThat(projectedP1.getField("z")).as("Should contain null z").isNull();
-
-    projectedP2 = points.get(1);
-    assertThat((int) projectedP2.getField("x")).as("Should project x").isEqualTo(3);
-    assertThat(projectedP2.getField("y")).as("Should project null y").isNull();
-    assertThat(projectedP2.getField("z")).as("Should contain null z").isNull();
+    assertThat(points)
+        .element(0)
+        .satisfies(
+            projectedP1 -> {
+              assertThat((int) projectedP1.getField("x")).isEqualTo(1);
+              assertThat((int) projectedP1.getField("y")).isEqualTo(2);
+              assertThat(projectedP1.getField("z")).isNull();
+            });
+    assertThat(points)
+        .element(1)
+        .satisfies(
+            projectedP2 -> {
+              assertThat((int) projectedP2.getField("x")).isEqualTo(3);
+              assertThat(projectedP2.getField("y")).isNull();
+              assertThat(projectedP2.getField("z")).isNull();
+            });
   }
 
   private static org.apache.avro.Schema fromOption(org.apache.avro.Schema schema) {
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
index a7334a580c..3d74b91359 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSnapshotSelection.java
@@ -24,7 +24,6 @@ import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
@@ -98,7 +97,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionById() throws IOException {
+  public void testSnapshotSelectionById() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -145,7 +144,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByTimestamp() throws IOException {
+  public void testSnapshotSelectionByTimestamp() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -197,7 +196,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByInvalidSnapshotId() throws IOException {
+  public void testSnapshotSelectionByInvalidSnapshotId() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -212,7 +211,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByInvalidTimestamp() throws IOException {
+  public void testSnapshotSelectionByInvalidTimestamp() {
     long timestamp = System.currentTimeMillis();
 
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
@@ -232,7 +231,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionBySnapshotIdAndTimestamp() throws IOException {
+  public void testSnapshotSelectionBySnapshotIdAndTimestamp() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -264,7 +263,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByTag() throws IOException {
+  public void testSnapshotSelectionByTag() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -300,7 +299,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByBranch() throws IOException {
+  public void testSnapshotSelectionByBranch() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -336,7 +335,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByBranchAndTagFails() throws IOException {
+  public void testSnapshotSelectionByBranchAndTagFails() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -367,7 +366,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByTimestampAndBranchOrTagFails() throws IOException {
+  public void testSnapshotSelectionByTimestampAndBranchOrTagFails() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -410,7 +409,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByBranchWithSchemaChange() throws IOException {
+  public void testSnapshotSelectionByBranchWithSchemaChange() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -467,7 +466,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testWritingToBranchAfterSchemaChange() throws IOException {
+  public void testWritingToBranchAfterSchemaChange() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
@@ -541,7 +540,7 @@ public class TestSnapshotSelection {
   }
 
   @TestTemplate
-  public void testSnapshotSelectionByTagWithSchemaChange() throws IOException {
+  public void testSnapshotSelectionByTagWithSchemaChange() {
     String tableLocation = temp.resolve("iceberg-table").toFile().toString();
 
     HadoopTables tables = new HadoopTables(CONF);
diff --git a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
index fb2b312bed..d2fde806f8 100644
--- a/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
+++ b/spark/v3.5/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkDataWrite.java
@@ -28,7 +28,6 @@ import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
@@ -112,7 +111,7 @@ public class TestSparkDataWrite {
   }
 
   @TestTemplate
-  public void testBasicWrite() throws IOException {
+  public void testBasicWrite() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -141,8 +140,7 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
     for (ManifestFile manifest :
         SnapshotUtil.latestSnapshot(table, branch).allManifests(table.io())) {
       for (DataFile file : ManifestFiles.read(manifest, table.io())) {
@@ -164,7 +162,7 @@ public class TestSparkDataWrite {
   }
 
   @TestTemplate
-  public void testAppend() throws IOException {
+  public void testAppend() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -211,12 +209,11 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testEmptyOverwrite() throws IOException {
+  public void testEmptyOverwrite() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -257,12 +254,11 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testOverwrite() throws IOException {
+  public void testOverwrite() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -310,12 +306,11 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testUnpartitionedOverwrite() throws IOException {
+  public void testUnpartitionedOverwrite() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -353,12 +348,11 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testUnpartitionedCreateWithTargetFileSizeViaTableProperties() throws IOException {
+  public void testUnpartitionedCreateWithTargetFileSizeViaTableProperties() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -393,8 +387,7 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
 
     List<DataFile> files = Lists.newArrayList();
     for (ManifestFile manifest :
@@ -404,29 +397,32 @@ public class TestSparkDataWrite {
       }
     }
 
-    assertThat(files).as("Should have 4 DataFiles").hasSize(4);
-    assertThat(files.stream())
-        .as("All DataFiles contain 1000 rows")
-        .allMatch(d -> d.recordCount() == 1000);
+    assertThat(files)
+        .hasSize(4)
+        .allSatisfy(
+            dataFile ->
+                assertThat(dataFile.recordCount())
+                    .as("All DataFiles contain 1000 rows")
+                    .isEqualTo(1000));
   }
 
   @TestTemplate
-  public void testPartitionedCreateWithTargetFileSizeViaOption() throws IOException {
+  public void testPartitionedCreateWithTargetFileSizeViaOption() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.NONE);
   }
 
   @TestTemplate
-  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption() throws IOException {
+  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.TABLE);
   }
 
   @TestTemplate
-  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption2() throws IOException {
+  public void testPartitionedFanoutCreateWithTargetFileSizeViaOption2() {
     partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType.JOB);
   }
 
   @TestTemplate
-  public void testWriteProjection() throws IOException {
+  public void testWriteProjection() {
     assumeThat(spark.version())
         .as("Not supported in Spark 3; analysis requires all columns are present")
         .startsWith("2");
@@ -459,12 +455,11 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testWriteProjectionWithMiddle() throws IOException {
+  public void testWriteProjectionWithMiddle() {
     assumeThat(spark.version())
         .as("Not supported in Spark 3; analysis requires all columns are present")
         .startsWith("2");
@@ -504,12 +499,11 @@ public class TestSparkDataWrite {
 
     List<ThreeColumnRecord> actual =
         result.orderBy("c1").as(Encoders.bean(ThreeColumnRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
   }
 
   @TestTemplate
-  public void testViewsReturnRecentResults() throws IOException {
+  public void testViewsReturnRecentResults() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -540,8 +534,7 @@ public class TestSparkDataWrite {
     List<SimpleRecord> actual1 =
         spark.table("tmp").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expected1 = Lists.newArrayList(new SimpleRecord(1, "a"));
-    assertThat(actual1).as("Number of rows should match").hasSameSizeAs(expected1);
-    assertThat(actual1).as("Result rows should match").isEqualTo(expected1);
+    assertThat(actual1).hasSameSizeAs(expected1).isEqualTo(expected1);
 
     df.select("id", "data")
         .write()
@@ -554,12 +547,10 @@ public class TestSparkDataWrite {
         spark.table("tmp").as(Encoders.bean(SimpleRecord.class)).collectAsList();
     List<SimpleRecord> expected2 =
         Lists.newArrayList(new SimpleRecord(1, "a"), new SimpleRecord(1, "a"));
-    assertThat(actual2).as("Number of rows should match").hasSameSizeAs(expected2);
-    assertThat(actual2).as("Result rows should match").isEqualTo(expected2);
+    assertThat(actual2).hasSameSizeAs(expected2).isEqualTo(expected2);
   }
 
-  public void partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType option)
-      throws IOException {
+  public void partitionedCreateWithTargetFileSizeViaOption(IcebergOptionsType option) {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "test");
     String targetLocation = locationWithBranch(location);
@@ -623,8 +614,7 @@ public class TestSparkDataWrite {
 
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSameSizeAs(expected);
-    assertThat(actual).as("Result rows should match").isEqualTo(expected);
+    assertThat(actual).hasSameSizeAs(expected).isEqualTo(expected);
 
     List<DataFile> files = Lists.newArrayList();
     for (ManifestFile manifest :
@@ -633,14 +623,17 @@ public class TestSparkDataWrite {
         files.add(file);
       }
     }
-    assertThat(files).as("Should have 8 DataFiles").hasSize(8);
-    assertThat(files.stream())
-        .as("All DataFiles contain 1000 rows")
-        .allMatch(d -> d.recordCount() == 1000);
+    assertThat(files)
+        .hasSize(8)
+        .allSatisfy(
+            dataFile ->
+                assertThat(dataFile.recordCount())
+                    .as("All DataFiles contain 1000 rows")
+                    .isEqualTo(1000));
   }
 
   @TestTemplate
-  public void testCommitUnknownException() throws IOException {
+  public void testCommitUnknownException() {
     File parent = temp.resolve(format.toString()).toFile();
     File location = new File(parent, "commitunknown");
     String targetLocation = locationWithBranch(location);
@@ -709,9 +702,8 @@ public class TestSparkDataWrite {
     Dataset<Row> result = spark.read().format("iceberg").load(targetLocation);
     List<SimpleRecord> actual =
         result.orderBy("id").as(Encoders.bean(SimpleRecord.class)).collectAsList();
-    assertThat(actual).as("Number of rows should match").hasSize(records.size() + records2.size());
     assertThat(actual)
-        .describedAs("Result rows should match")
+        .hasSize(records.size() + records2.size())
         .containsExactlyInAnyOrder(
             ImmutableList.<SimpleRecord>builder()
                 .addAll(records)