blob: 05d0ec4ca72bcc2ea4d0fef20b67d3c3322d26f1 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Writing tests (JUnit 5 + AssertJ)
Conventions for **writing new tests** and **reviewing new/changed tests** in this
repository using [AssertJ](https://assertj.github.io/doc/). Use JUnit 5
(`org.junit.jupiter.api.*`) for test lifecycle and AssertJ for all assertions,
exceptions, and assumptions.
> Migrating an existing JUnit 4 test to this style? Start with
> [`migrating-tests.md`](./migrating-tests.md) — it covers the mechanical first
> pass, the JUnit → AssertJ mapping table, and the post-migration audit. The
> conventions below define the **target style** those migrations aim for.
## Imports
- Use `import static org.assertj.core.api.Assertions.assertThat;`
- Use `import static org.assertj.core.api.Assertions.assertThatThrownBy;` for exception tests
- Use `import static org.assertj.core.api.Assertions.assertThatCode;` when verifying no exception is thrown
- Use `import static org.assertj.core.api.Assumptions.assumeThat;` for conditional test execution (skip when assumption fails) — **not** JUnit `Assume`, JUnit 5 `Assumptions`, or any other assumption API
- **Do not** import from `AssertionsForClassTypes` unless there is a genuine generic ambiguity (e.g. `invoke()` return values). Prefer `Assertions` first.
- Use JUnit 5 (`org.junit.jupiter.api.*`) for test lifecycle only — `@Test`, `@BeforeEach`, `@AfterEach`, `@TempDir`, etc. Use AssertJ for assertions, exceptions, and assumptions.
## `@TempDir`
Use JUnit 5 `@TempDir` instead of JUnit 4 `TemporaryFolder` / `@Rule`:
```java
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
@TempDir
private Path tempDir;
```
- Declare `@TempDir` fields **`private`** by default — not package-private or `public`.
- Prefer `java.nio.file.Path` on the test class; expose a `protected` accessor (e.g. `getTempFolder()`) when subclasses need the directory.
- When both `java.nio.file.Path` and `org.apache.hadoop.fs.Path` are in scope, declare `@TempDir` as `private java.nio.file.Path tempDir` and **do not** import `java.nio.file.Path` if it would clash with Hadoop `Path`.
```java
// Wrong — package-private or public
@TempDir
Path tempDir;
// Wrong — JUnit 4
@Rule
public TemporaryFolder temp = new TemporaryFolder();
// Correct
@TempDir
private Path tempDir;
// Correct when Hadoop Path is also used
@TempDir
private java.nio.file.Path tempDir;
```
### Path handling (`@TempDir` → Hadoop `Path` / `File`)
Stay on `java.nio.file.Path` for temp-dir work. Convert to Hadoop or `java.io.File` only at API boundaries.
**Hadoop `Path` from `@TempDir`:**
```java
// Correct — child path for a writer/reader
Path output = new Path(tempDir.resolve("out.parquet").toUri());
// Correct — unique file that must not exist yet (CREATE mode)
Path output = new Path(tempDir.resolve(UUID.randomUUID().toString()).toUri());
// Correct — directory root
Path root = new Path(tempDir.toUri());
```
Prefer `.toUri()` when constructing Hadoop `Path` from `java.nio.file.Path` — do not round-trip through `toFile().getAbsolutePath()` or `toString()` on an intermediate variable unless the API requires a `String`.
**`java.io.File` when required:**
```java
File file = tempDir.resolve("child.parquet").toFile();
```
Use `tempDir.resolve("name")`, not `new File(tempDir.toFile(), "name")`.
**Do not pre-create files for Hadoop CREATE-mode writers.** Parquet/Hadoop writers open with create-if-not-exists semantics and fail with `FileAlreadyExistsException` when the path already exists. A UUID (or other unique name) under `@TempDir` is enough — no `Files.createFile` / `temp.newFile()` + delete dance.
```java
// Wrong — creates then deletes, or leaves a file that breaks CREATE
File temp = Files.createTempFile(tempDir, "test", ".tmp").toFile();
Path path = new Path(temp.getAbsolutePath());
java.nio.file.Path tempFile = Files.createFile(tempDir.resolve(UUID.randomUUID().toString()));
Files.delete(tempFile);
Path path = new Path(tempFile.toString());
// Wrong — redundant conversion
new Path(tempDir.toFile().getAbsolutePath())
new Path(new File(tempDir.toFile(), "out").getAbsolutePath())
// Correct — path does not exist yet; writer creates it
Path path = new Path(tempDir.resolve(UUID.randomUUID().toString()).toUri());
```
**`Files.createTempFile` / `Files.createTempDirectory`:** keep only when the test needs JDK temp-name generation inside `@TempDir` (e.g. a specific prefix/suffix). If the created path is passed to a CREATE-mode writer, delete it first with `Files.delete(path)`. Prefer `tempDir.resolve(...)` or `Files.createTempDirectory(tempDir, "prefix")` + `new Path(dir.toUri())` over `.toFile().getAbsolutePath()`.
**Shared test bases:** use `org.apache.parquet.DirectWriterTest` from the `parquet-hadoop` test-jar for direct `RecordConsumer` writing — do not duplicate it in other modules (e.g. no module-local `AvroDirectWriterTest`).
```java
import org.apache.parquet.DirectWriterTest;
public class TestArrayCompatibility extends DirectWriterTest {
```
## Actual vs expected order
AssertJ uses **actual first, expected second**:
```java
// Correct
assertThat(stream.read()).isEqualTo(124);
assertThat(column("a").compareTo(column("b"))).isEqualTo(-1);
assertThat(cw.getEncoding()).isEqualTo(PLAIN_DICTIONARY);
// Wrong (flipped)
assertThat(124).isEqualTo(stream.read());
assertThat(PLAIN_DICTIONARY).isEqualTo(cw.getEncoding());
```
## Assumptions
Use AssertJ `assumeThat` (`import static org.assertj.core.api.Assumptions.assumeThat`) to skip tests when a precondition is not met (OS-specific tests, optional features, etc.). Do **not** use JUnit 4 `Assume`, JUnit 5 `org.junit.jupiter.api.Assumptions`, or any JUnit assumption helper.
Prefer fluent assertions on the value under test over wrapping a boolean expression in `.isTrue()`:
```java
// Wrong — JUnit 4
Assume.assumeTrue(System.getProperty("os.name").toLowerCase().startsWith("win"));
// Wrong — JUnit 5
Assumptions.assumeTrue(System.getProperty("os.name").toLowerCase().startsWith("win"));
// Wrong — JUnit 5 static import of Assumptions (still JUnit, not AssertJ)
import static org.junit.jupiter.api.Assumptions.*;
assumeTrue(featureEnabled);
// Correct — AssertJ assumeThat
assumeThat(featureEnabled).isTrue();
// Correct — fluent check on the subject
assumeThat(System.getProperty("os.name").toLowerCase()).startsWith("win");
```
When an assumption fails, AssertJ throws `org.opentest4j.TestAbortedException` (same as JUnit 5), so the test is reported as skipped.
## Exception assertions
Use `assertThatThrownBy` for expected exceptions:
```java
assertThatThrownBy(() -> action())
.isInstanceOf(SomeException.class)
.hasMessage("exact message");
```
Every `assertThatThrownBy` chain **must** include `.isInstanceOf(...)` **and** a message assertion (`.hasMessage`, `.hasMessageContaining`, or `.hasMessageStartingWith`) unless the thrown exception reliably has a `null` message (see below).
**Always** use `assertThatThrownBy` — not `catchThrowable` followed by `assertThat(caught)`:
```java
// Wrong — split capture and assertion; does not fail clearly when no exception is thrown
Throwable caught = catchThrowable(() -> action());
assertThat(caught).isInstanceOf(BadConfigurationException.class);
assertThat(caught).hasMessageContaining("23");
// Correct
assertThatThrownBy(() -> action())
.isInstanceOf(BadConfigurationException.class)
.hasMessageContaining("23");
```
Use `.satisfies(…)`, `.hasSuppressedException(…)`, `.hasNoSuppressedExceptions()`, or `.isSameAs(…)` on the `assertThatThrownBy` chain when additional checks on the thrown instance are needed. Reserve `catchThrowable` only when the test must **continue after** capturing (e.g. multiple independent actions in one test) — not for a single expected failure.
`isInstanceOf` alone is **not** sufficient — it loses coverage of the error text users and operators actually see.
### Message checks
- **Always** add `.hasMessage(...)`, `.hasMessageContaining(...)`, or `.hasMessageStartingWith(...)` when the exception has a non-null message.
- Use `.hasMessage(...)` when the full message is stable and known (copy it from the `throw` site or a failing test).
- Use `.hasMessageContaining(...)` when the message includes variable detail (type names, field paths, etc.) or when several loop iterations share a common substring.
- **Omit** `hasMessage` only when the JDK exception message is reliably `null` (e.g. `EOFException`, `InvalidMarkException`, `ReadOnlyBufferException`, re-thrown checked exceptions with no message).
- **Never** use `.hasMessage(null)` or `.hasMessage((String) null)`.
```java
// Wrong — type only
assertThatThrownBy(() -> new AvroSchemaConverter().convert(parquetSchemaWithInt96))
.isInstanceOf(IllegalArgumentException.class);
// Correct
assertThatThrownBy(() -> new AvroSchemaConverter().convert(parquetSchemaWithInt96))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"INT96 is deprecated. As interim enable READ_INT96_AS_FIXED flag to read as byte array.");
// Correct — shared substring across similar failures
assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot annotate schema");
```
### `assertThatThrownBy` style
- **Do not** put `.as("description")` on `assertThatThrownBy` chains. Rely on the test method name or inline the intent in the lambda.
- **Do** keep `.as(DECIMAL)`, `.as(logicalType)`, etc. on **builder** chains inside the lambda — those are not AssertJ descriptions.
- Use an **expression lambda** for a single action. Reserve block lambdas (`() -> { … }`) for multiple statements.
```java
// Wrong — unnecessary block for one call
assertThatThrownBy(() -> {
evaluate(neverCalled);
})
.isInstanceOf(ShortCircuitException.class)
.hasMessage("…");
// Correct — expression lambda (or method reference when it fits)
assertThatThrownBy(() -> evaluate(neverCalled))
.isInstanceOf(ShortCircuitException.class)
.hasMessage("…");
assertThatThrownBy(command::run)
.isInstanceOf(FileAlreadyExistsException.class)
.hasMessageContaining("File already exists");
// Block lambda is fine when setup is required
assertThatThrownBy(() -> {
MessageType incompatible = new MessageType("schema", …);
readGroups(store, originalSchema, incompatible, 1);
})
.isInstanceOf(ParquetDecodingException.class)
.hasMessage("…");
```
```java
// Wrong
assertThatThrownBy(() -> builder.build())
.as("Should reject invalid type")
.isInstanceOf(IllegalArgumentException.class);
// Correct
assertThatThrownBy(() -> builder.build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("…");
// Builder .as() inside lambda is fine
assertThatThrownBy(() -> Types.required(INT32).as(DECIMAL).named("x"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("…");
```
### No exception expected
Replace no-op `assertTrue(true)` after a successful call, try/catch/`Assert.fail` blocks, or Java `assert` statements with:
```java
assertThatCode(() -> uuidConverter.addBinary(binary)).doesNotThrowAnyException();
assertThatCode(() -> ToolRunner.run(conf, new Main(logger), args)).doesNotThrowAnyException();
```
## Numeric results
Prefer dedicated numeric assertions over boolean comparisons:
```java
// Wrong
assertThat(command.run()).isEqualTo(0);
assertThat(0 < file.length()).isTrue();
assertThat(columnSizeInBytes.get("DocId") > columnSizeInBytes.get("Num")).isTrue();
// Correct
assertThat(command.run()).isZero();
assertThat(file.length()).isPositive();
assertThat(columnSizeInBytes.get("DocId")).isGreaterThan(columnSizeInBytes.get("Num"));
```
Use `.isZero()`, `.isPositive()`, `.isNegative()`, `.isGreaterThan(...)`, etc. on the **value under test**.
## Files
```java
// Wrong
assertThat(output.exists()).isTrue();
assertThat(outputFile.length()).isEqualTo(0);
// Correct
assertThat(output).exists();
assertThat(outputFile.length()).isZero();
assertThat(avroFile.length()).isPositive();
```
## String / `toString()` comparisons
Use AssertJ's `.asString()` on the **subject**, not `.toString()` in `assertThat(...)`:
```java
// Wrong
assertThat(schema.toString()).isEqualTo(expectedMT.toString());
assertThat(stats.toString()).isEqualTo("min: 1.0, max: 2.0, num_nulls: 0");
// Correct — object compared to object (use .toString() on expected to avoid format overload)
assertThat(schema).asString().isEqualTo(expectedMT.toString());
// Correct — object compared to string literal
assertThat(stats).asString().isEqualTo("min: 1.0, max: 2.0, num_nulls: 0");
assertThat(pred).asString().isEqualTo("or(and(not(…");
assertThat(schemaString).contains("\"name\" : \"timestamp_1\"");
assertThat(schemaString).doesNotContain("\"type\" : [ \"null\", \"INT96\" ]");
// Correct — builder chain
assertThat(Types.required(FIXED_LEN_BYTE_ARRAY).length(16).as(uuidType()).named("uuid_field"))
.asString()
.isEqualTo("required fixed_len_byte_array(16) uuid_field (UUID)");
```
**Important:** `asString().isEqualTo(otherObject)` can hit AssertJ's `isEqualTo(String format, Object...)` overload. When comparing to another object, use `.isEqualTo(other.toString())` on the expected side.
`.as("description")` **before** `.asString()` is allowed on normal assertions:
```java
assertThat(records.get(0))
.as("deserialization does not display the same result")
.asString()
.isEqualTo(r1.toString());
```
## Collections and sizes
Prefer AssertJ **collection assertions** on the collection itself. Assert on the collection (`list`, `conversions`, `records`, `encodings`, …), not on `size()`, `isEmpty()`, or `contains()` return values.
### Size
**Always** use `hasSize(n)` instead of `assertThat(collection.size()).isEqualTo(n)` or `assertThat(array.length).isEqualTo(n)`.
When comparing two collections (or a collection and an array) that should have the same length, prefer `hasSameSizeAs` over `hasSize(other.size())` or `hasSize(array.length)`:
```java
// Wrong
assertThat(splits.size()).isEqualTo(offsets.length);
assertThat(splits).hasSize(sizes.length);
assertThat(actual.size()).isEqualTo(expected.size());
// Correct
assertThat(splits).hasSameSizeAs(offsets);
assertThat(splits).hasSameSizeAs(sizes);
assertThat(actual).hasSameSizeAs(expected);
```
Use `hasSizeLessThan` / `hasSizeGreaterThan` when asserting a relative size bound, not a boolean comparison on sizes:
```java
// Wrong
assertThat(result.size() < DATA.size()).isTrue();
// Correct
assertThat(result).hasSizeLessThan(DATA.size());
```
### Membership, emptiness, and equality
Do not wrap collection state in a boolean and assert with `isTrue()` / `isFalse()`:
```java
// Wrong
assertThat(column.getEncodings().contains(Encoding.PLAIN_DICTIONARY))
.as("Column should be dictionary encoded: " + name)
.isTrue();
assertThat(column.getEncodings().contains(Encoding.PLAIN))
.as("Column should not have plain data pages" + name)
.isFalse();
assertThat(list).isEqualTo(List.of(1, 2, 3));
assertThat(list).isEqualTo(Collections.emptyList());
assertThat(list.size()).isEqualTo(3);
assertThat(list.isEmpty()).isTrue();
assertThat(!list.isEmpty()).isFalse();
assertThat(list.contains(item)).isTrue();
assertThat(metadata.getBlocks().isEmpty()).isFalse();
assertThat(metadata.getBlocks().size() > 0).isTrue();
// Correct
assertThat(column.getEncodings())
.as("Column should be dictionary encoded: " + name)
.contains(Encoding.PLAIN_DICTIONARY);
assertThat(column.getEncodings())
.as("Column should not have plain data pages" + name)
.doesNotContain(Encoding.PLAIN);
assertThat(list).containsExactly(1, 2, 3);
assertThat(list).isEmpty();
assertThat(list).isNotEmpty();
assertThat(list).hasSize(3);
assertThat(list).contains(item);
assertThat(metadata.getBlocks()).isNotEmpty();
```
Use `containsExactly` for ordered list equality with literal elements. Use `containsExactlyElementsOf` when comparing to another iterable. Use `containsExactlyInAnyOrder` only when order does not matter (e.g. `HashSet`-backed iterables).
**Exception:** `assertThat(stream.size()).isEqualTo(n)` on `BytesInput` / stream-like types is fine — `hasSize()` applies to collections, not arbitrary `size()` methods.
**Exception:** `Statistics.isEmpty()`, `hasNonNullValue()`, and similar domain `boolean` methods are not Java collections — keep `assertThat(stats.isEmpty()).isFalse()` or assert the specific stat fields directly.
## Optional (`java.util.Optional`)
Assert on the **Optional**, not on `optional.isPresent()` / `optional.isEmpty()` wrapped in `isTrue()` / `isFalse()`:
```java
// Wrong
assertThat(page.getCrc().isPresent()).as("Checksum was not set in page").isTrue();
assertThat(page.getCrc().isPresent()).as("Checksum was set in page").isFalse();
assertThat(offsetIndex.getUnencodedByteArrayDataBytes(0).isPresent()).isFalse();
// Correct
assertThat(page.getCrc()).as("Checksum was not set in page").isPresent();
assertThat(page.getCrc()).as("Checksum was set in page").isEmpty();
assertThat(offsetIndex.getUnencodedByteArrayDataBytes(0)).isEmpty();
```
For a present value, prefer `assertThat(opt).contains(value)` or `assertThat(opt).get().isEqualTo(value)` over `assertThat(opt.isPresent()).isTrue()` followed by `opt.get()`.
**Exception:** `Statistics.isEmpty()` and other domain `boolean` methods are not `Optional` — see Collections above.
## Streams and iterators
### Iterator content
Replace custom helpers that drain an iterator into a list/array and compare with `isEqualTo` (e.g. `assertIteratorEquals`, `assertAllRowsEqual`). Use AssertJ's `IteratorAssert.toIterable()` to consume the iterator and apply iterable assertions:
```java
// Wrong — custom helper draining the iterator
static void assertIteratorEquals(PrimitiveIterator.OfInt actualIt, int... expectedValues) {
IntList actualList = new IntArrayList();
actualIt.forEachRemaining(actualList::add);
assertThat(actualList.toIntArray()).isEqualTo(expectedValues);
}
assertIteratorEquals(IndexIterator.all(10), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
// Correct — fluent AssertJ on the iterator
assertThat(IndexIterator.all(10)).toIterable().containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
assertThat(IndexIterator.intersection(lhs, rhs)).toIterable().isEmpty();
assertThat(ranges.iterator()).toIterable().containsExactly(1L, 2L, 3L, 4L);
```
Notes:
- `PrimitiveIterator.OfInt` / `OfLong` extend `Iterator`, so `assertThat(iterator)` works directly.
- `.toIterable()` **consumes** the iterator (same as a drain helper). Non-consuming checks use `hasNext()` / `isExhausted()` on the iterator itself (see below).
- For `PrimitiveIterator.OfLong`, `containsExactly` expects `Long` varargs — use `L` suffixes on literals (`1L`, `2L`, …).
- For `int...` expected values passed as a varargs parameter, box before spreading into `containsExactly`:
```java
assertThat(predicate.accept(ci))
.toIterable()
.containsExactly(Arrays.stream(expectedIndexes).boxed().toArray(Integer[]::new));
```
Do **not** add a custom AssertJ assertion class unless the same pattern is needed across many modules — `toIterable()` is sufficient.
### Iterator state (`hasNext` / `isExhausted`)
Assert on the **iterator**, not on `iterator.hasNext()` wrapped in `isTrue()` / `isFalse()`:
```java
// Wrong
assertThat(expIt.hasNext()).isFalse();
assertThat(expIt.hasNext()).isTrue();
// Correct
assertThat(expIt).isExhausted();
assertThat(expIt).hasNext();
```
Use `.isExhausted()` when all elements were consumed (e.g. after a partial match loop). Use `.hasNext()` when more elements are expected. `.as("…")` before these is allowed when the failure message needs context.
### `java.util.stream.Stream`
AssertJ has no `StreamAssert` — **collect first**, then assert on the resulting collection:
```java
// Wrong — boolean wrapper on stream terminal operation
assertThat(result.size() == expected.size()).isTrue();
// Wrong — isEqualTo on two independently collected lists (often fine, but prefer iterable API)
assertThat(result).isEqualTo(DATA.stream().filter(pred).collect(Collectors.toList()));
// Correct — collect actual, then compare as iterable
List<User> result = readFilteredUsers(filter);
assertThat(result).containsExactlyElementsOf(DATA.stream().filter(expectedFilter).collect(Collectors.toList()));
// Correct — known expected elements
assertThat(result).containsExactly(userA, userB);
```
For a `Stream` used only as an expected source in a helper, passing `stream.iterator()` to `assertThat(it).toIterable()` is fine when the stream is consumed once.
When asserting a **Java array** length, prefer `assertThat(array).hasSize(n)` over `assertThat(array.length).isEqualTo(n)`.
### Maps and sets — combine related checks
Prefer one assertion when size and membership are asserted together:
```java
// Wrong — three assertions for the same map
assertThat(map).hasSize(2);
assertThat(map).containsKey(keyA);
assertThat(map).containsKey(keyB);
// Correct
assertThat(map).containsOnlyKeys(keyA, keyB);
// Wrong — size + contains on a set
assertThat(set).hasSize(2);
assertThat(set).contains("hello", "world");
// Correct
assertThat(set).containsExactlyInAnyOrder("hello", "world");
```
Use `containsOnlyKeys` when the map must have **exactly** those keys. Use `containsExactlyInAnyOrder` for sets (order irrelevant). Use `containsExactly` when order matters.
### When `isTrue()` / `isFalse()` is still fine
Use `isTrue()` / `isFalse()` for genuine boolean expressions that are not collection, string, **type**, **reference equality**, or **membership** checks, e.g. `filter.keep(path)` or method calls that return `boolean` without a dedicated AssertJ assertion.
Do **not** use `assertThat(x instanceof Foo.class).isTrue()` — use `assertThat(x).isInstanceOf(Foo.class)` instead. For negation, use `isNotInstanceOf(...)`.
Do **not** use `assertThat(a == b).isTrue()` for reference equality — use `assertThat(a).isSameAs(b)` instead.
Do **not** use `assertThat(x == a || x == b).isTrue()` for “one of these values” — use `assertThat(x).isIn(a, b)` instead (or `isNotIn(...)` for negation).
```java
// Wrong
assertThat(records.get(0).get("dec") instanceof BigDecimal).isTrue();
assertThat(obj instanceof Map).as("Should be a map").isTrue();
assertThat(car.getDoors() == 4 || car.getDoors() == 5).isTrue();
// Correct
assertThat(records.get(0).get("dec")).isInstanceOf(BigDecimal.class);
assertThat(obj).as("Should be a map").isInstanceOf(Map.class);
assertThat(car.getDoors()).isIn(4, 5);
```
## Ordering and comparison
### `Comparable` / `compareTo`
```java
// Wrong
assertThat(a.compareTo(b) < 0).isTrue();
assertThat(a.compareTo(b) == 0).isTrue();
// Correct
assertThat(a).isLessThan(b);
assertThat(a).isGreaterThan(b);
assertThat(a).isEqualByComparingTo(b);
```
### `compare()` returning `int`
When the API returns `int` (not `Comparable` on the subject), assert on the result:
```java
assertThat(Float16.compare(a, b)).isZero();
assertThat(Float16.compare(a, b)).isPositive();
assertThat(Float16.compare(a, b)).isNegative();
```
For custom `Comparator` instances comparing values directly:
```java
assertThat(truncated)
.usingComparator(comparator)
.isLessThanOrEqualTo(value);
```
Do **not** use `usingComparator` with `Float16::compare` on boxed `Short` values — it does not match `Float16.compare(short, short)` semantics.
### Reference equality (`isSameAs`)
The **value under test** is the AssertJ subject; the known reference is the argument:
```java
// Correct — slice buffer is under test
assertThat(one.array()).as("Should use the same backing array").isSameAs(data.array());
// Correct — dictionary-encoded field should reuse the same object reference
assertThat(car.getModel()).isSameAs(previousCar.getModel());
// Wrong — backing array as subject
assertThat(data.array()).isSameAs(one.array());
// Wrong — reference equality via isTrue()
assertThat(car.model == previousCar.model).isTrue();
```
`.as("…")` on non-exception assertions is allowed.
## Arrays and bytes
```java
// Prefer
assertThat(bytes).containsExactly(1, 2, 3);
assertThat(bytes).hasSize(12);
assertThat(buffer).isEqualTo(expectedArray);
// Byte values from streams — cast to avoid JUnit-style promotion issues
assertThat((int) stream.read()).isEqualTo(i);
assertThat((byte) buffer.get()).isEqualTo((byte) i);
```
## Booleans and filters
Replace custom match helpers with direct AssertJ:
```java
// Wrong
assertMatches(filter, path);
assertDoesNotMatch(filter, path);
// Correct
assertThat(filter.keep(path)).isTrue();
assertThat(filter.keep(path)).isFalse();
```
## Patterns to avoid (quick reference)
| Avoid | Use instead |
|-------|-------------|
| `catchThrowable` + `assertThat(caught).isInstanceOf` / `hasMessage*` | `assertThatThrownBy(…).isInstanceOf(…).hasMessage*` |
| `assertThatThrownBy` without `hasMessage` | Add `hasMessage` / `hasMessageContaining` from the `throw` site |
| `@Rule TemporaryFolder` / `temp.newFile()` | `@TempDir private Path tempDir` + `tempDir.resolve(...)` |
| `tempDir.toFile().getAbsolutePath()` / `new File(tempDir.toFile(), …)` | `tempDir.resolve(…).toUri()` or `.toFile()` |
| `new Path(tempDir.toFile().getAbsolutePath())` | `new Path(tempDir.resolve(…).toUri())` |
| `Files.createFile` + `Files.delete` before CREATE-mode writer | `tempDir.resolve(uniqueName)` only — file must not exist |
| Duplicate `DirectWriterTest` in other modules | `extends DirectWriterTest` from `parquet-hadoop` test-jar |
| `Assume.assumeTrue(...)` / `Assumptions.assumeTrue(...)` | AssertJ `assumeThat(...).isTrue()` or fluent `assumeThat(value).…` |
| `singleElement()` | `containsExactly(element)` or explicit checks |
| `.satisfies(…)` | Direct assertions unless truly necessary |
| `assertThat(x.toString())` | `assertThat(x).asString()` |
| `assertThat(list.isEmpty()).isTrue()` | `assertThat(list).isEmpty()` |
| `assertThat(list.isEmpty()).isFalse()` / `assertThat(!list.isEmpty()).isTrue()` | `assertThat(list).isNotEmpty()` |
| `assertThat(opt.isPresent()).isTrue()` | `assertThat(opt).isPresent()` |
| `assertThat(opt.isPresent()).isFalse()` / `assertThat(opt.isEmpty()).isTrue()` | `assertThat(opt).isEmpty()` |
| `assertThat(list.contains(x)).isTrue()` | `assertThat(list).contains(x)` |
| `assertThat(list.contains(x)).isFalse()` | `assertThat(list).doesNotContain(x)` |
| `assertThat(list).isEqualTo(otherList)` | `assertThat(list).containsExactlyElementsOf(otherList)` |
| `assertThat(list.size()).isEqualTo(n)` | `assertThat(list).hasSize(n)` |
| `assertThat(list).hasSize(other.size())` / `hasSize(array.length)` | `assertThat(list).hasSameSizeAs(other)` |
| `assertThat(a.size() < b.size()).isTrue()` | `assertThat(a).hasSizeLessThan(b)` |
| `assertThat(array.length).isEqualTo(n)` | `assertThat(array).hasSize(n)` |
| `assertThat(it).toIterable()` for drained iterators | `assertThat(it).toIterable().containsExactly(1, 2, 3)` |
| `assertThat(it.hasNext()).isFalse()` | `assertThat(it).isExhausted()` |
| `assertThat(it.hasNext()).isTrue()` | `assertThat(it).hasNext()` |
| `assertThat(list).isEqualTo(stream.collect(toList()))` | `assertThat(list).containsExactlyElementsOf(stream.collect(toList()))` |
| `hasSize(n)` + `containsKey` / `contains` on same map or set | `containsOnlyKeys(…)` / `containsExactlyInAnyOrder(…)` |
| `assertThat(x instanceof Foo).isTrue()` | `assertThat(x).isInstanceOf(Foo.class)` |
| `assertThat(a == b).isTrue()` | `assertThat(a).isSameAs(b)` |
| `assertThat(x == a \|\| x == b).isTrue()` | `assertThat(x).isIn(a, b)` |
| `assertThat(x).isEqualTo(0)` on `int`/`long` results | `assertThat(x).isZero()` |
| `assertThat(0 < x).isTrue()` | `assertThat(x).isPositive()` |
| `assertThat(file.exists()).isTrue()` | `assertThat(file).exists()` |
| `assertThat(true).isTrue()` / try-catch for no exception | `assertThatCode(() -> …).doesNotThrowAnyException()` |
| `.as()` on `assertThatThrownBy` | (omit) |
| `assertThatThrownBy(() -> { action(); })` (single statement) | `assertThatThrownBy(() -> action())` or method reference |
| `hasMessage(null)` | (omit `hasMessage`) |
| Constant as assertion subject | Expression under test as subject |
| `hasCauseInstanceOf` when exception is thrown directly | Use `isInstanceOf` on the thrown type |
## Verification
After writing or changing tests, apply formatting and run tests for the affected module:
```bash
./mvnw spotless:apply -pl <module>
mvn -pl <module> test
```