FluentAssert is a Kotlin library that provides fluent assertions for JDK types, making your tests more readable and expressive. The library wraps AssertJ assertions with Kotlin extension functions for better syntax.
- Fluent API: Chain assertions in a readable, natural way
- Null-Safe: All extension functions handle nullable types appropriately
- Comprehensive Coverage: Supports all major JDK types and collections
- Type-Safe: Full Kotlin type system integration
- AssertJ Powered: Leverages the robust AssertJ assertion library
- Zero Dependencies: Only depends on AssertJ (transitively)
- Kotlin Idiomatic: Designed specifically for Kotlin developers
- IDE Support: Full IntelliJ IDEA and Android Studio integration
- Java: 17 or higher
- Kotlin: 1.8.0 or higher
- JUnit: 5.x (for testing)
<dependency>
<groupId>me.ahoo.test</groupId>
<artifactId>fluent-assert-core</artifactId>
<version>0.2.2</version>
<scope>test</scope>
</dependency>testImplementation("me.ahoo.test:fluent-assert-core:0.2.2")testImplementation 'me.ahoo.test:fluent-assert-core:0.2.2'Simply call .assert() on any JDK type to start building fluent assertions:
import me.ahoo.test.asserts.assert
// Basic assertions
val name = "FluentAssert"
name.assert().startsWith("Fluent").endsWith("Assert")
val age = 25
age.assert().isGreaterThan(18).isLessThan(100)
val isActive = true
isActive.assert().isTrue()This project includes LLM-friendly documentation files:
llms.txt: Concise project overview for AI assistantsllms-full.txt: Complete API reference and technical detailsAGENTS.md: Development guidelines for coding agents
Use these files to understand the project structure, API patterns, and coding standards when contributing.
All extension functions follow the pattern Type.assert(): AssertJTypeAssert, where:
Typeis any supported JDK type (nullable or non-nullable)AssertJTypeAssertis the corresponding AssertJ assertion class
| Category | Types |
|---|---|
| Primitives | Boolean, Byte, Short, Int, Long, Float, Double, BigDecimal |
| Text | String |
| Collections | Iterable<T>, Iterator<T>, Collection<T>, Array<T>, List<T>, Map<K,V>, Optional<T>, Stream<T> |
| Time/Date | Date, ZonedDateTime, LocalDateTime, OffsetDateTime, OffsetTime, LocalTime, LocalDate, YearMonth, Instant, Duration, Period, Temporal |
| I/O | Path, File, URL, URI |
| Concurrent | Future<V>, CompletableFuture<V>, CompletionStage<V> |
| Functional | Predicate<T> |
| Exceptions | Throwable |
assertThrownBy<T : Throwable>(shouldRaiseThrowable: () -> Unit): ThrowableAssert<T>- Asserts that code throws a specific exception typeThrowable.assert(): ThrowableAssert<Throwable>- Creates assertions for exception instances
Creates assertions for boolean values.
true.assert().isTrue()
false.assert().isFalse()
val nullableBool: Boolean? = null
nullableBool.assert().isNull()Creates assertions for byte values.
val value: Byte = 42
value.assert().isEqualTo(42).isPositive()Creates assertions for short values.
val value: Short = 1000
value.assert().isEqualTo(1000).isGreaterThan(0)Creates assertions for integer values.
val age = 25
age.assert().isEqualTo(25).isBetween(18, 65)Creates assertions for long values.
val timestamp = System.currentTimeMillis()
timestamp.assert().isPositive().isGreaterThan(0)Creates assertions for float values.
val pi = 3.14f
pi.assert().isEqualTo(3.14f).isPositive()Creates assertions for double values.
val price = 19.99
price.assert().isEqualTo(19.99).isPositive()Creates assertions for BigDecimal values.
val amount = BigDecimal("123.45")
amount.assert().isEqualTo("123.45").isPositive()Creates assertions for string values.
val name = "FluentAssert"
name.assert()
.startsWith("Fluent")
.endsWith("Assert")
.contains("uentAss")
.hasLength(11)Creates assertions for any object type.
val person = Person("John", 30)
person.assert()
.isNotNull()
.hasFieldOrPropertyWithValue("name", "John")Creates assertions for comparable objects.
val version = "2.0.0"
version.assert()
.isGreaterThan("1.0.0")
.isLessThan("3.0.0")Creates assertions for iterable collections.
val numbers = listOf(1, 2, 3, 4, 5)
numbers.assert()
.hasSize(5)
.contains(3)
.doesNotContain(6)
.allMatch { it > 0 }Creates assertions for iterators.
val iterator = listOf(1, 2, 3).iterator()
iterator.assert().hasNext()Creates assertions for collections.
val set = setOf("apple", "banana", "orange")
set.assert()
.hasSize(3)
.contains("apple")
.doesNotContain("grape")Creates assertions for arrays.
val array = arrayOf("a", "b", "c")
array.assert()
.hasSize(3)
.contains("b")
.doesNotContain("d")Creates assertions for lists.
val items = listOf("apple", "banana", "orange")
items.assert()
.hasSize(3)
.contains("apple", "banana")
.element(0).isEqualTo("apple")Creates assertions for Optional values.
val present = Optional.of("value")
present.assert()
.isPresent()
.contains("value")
val empty = Optional.empty<String>()
empty.assert().isEmpty()Creates assertions for maps.
val map = mapOf("key1" to "value1", "key2" to "value2")
map.assert()
.hasSize(2)
.containsKey("key1")
.containsValue("value1")
.containsEntry("key1", "value1")Creates assertions for streams (converted to lists).
val stream = listOf(1, 2, 3, 4, 5).stream()
stream.assert()
.hasSize(5)
.contains(3)
.allMatch { it > 0 }Creates assertions for Date objects.
val date = Date()
date.assert()
.isToday()
.isBefore(Date(System.currentTimeMillis() + 1000))Creates assertions for ZonedDateTime objects.
val zonedDateTime = ZonedDateTime.now()
zonedDateTime.assert()
.isToday()
.hasZone(ZoneId.systemDefault())Creates assertions for any Temporal objects.
val instant = Instant.now()
instant.assert()
.isBefore(Instant.now().plusSeconds(1))Creates assertions for LocalDateTime objects.
val dateTime = LocalDateTime.now()
dateTime.assert()
.isToday()
.isBefore(LocalDateTime.now().plusHours(1))Creates assertions for OffsetDateTime objects.
val offsetDateTime = OffsetDateTime.now()
offsetDateTime.assert()
.isToday()
.hasOffset(ZoneOffset.UTC)Creates assertions for OffsetTime objects.
val offsetTime = OffsetTime.now()
offsetTime.assert()
.isBefore(OffsetTime.now().plusHours(1))Creates assertions for LocalTime objects.
val time = LocalTime.of(10, 30)
time.assert()
.isBefore(LocalTime.of(12, 0))
.hasHour(10)
.hasMinute(30)Creates assertions for LocalDate objects.
val date = LocalDate.of(2023, 12, 25)
date.assert()
.hasYear(2023)
.hasMonth(12)
.hasDayOfMonth(25)Creates assertions for YearMonth objects.
val yearMonth = YearMonth.of(2023, 12)
yearMonth.assert()
.hasYear(2023)
.hasMonth(12)Creates assertions for Instant objects.
val instant = Instant.now()
instant.assert()
.isBefore(Instant.now().plusSeconds(1))Creates assertions for Duration objects.
val duration = Duration.ofHours(2)
duration.assert()
.hasHours(2)
.isGreaterThan(Duration.ofHours(1))Creates assertions for Period objects.
val period = Period.of(1, 2, 3)
period.assert()
.hasYears(1)
.hasMonths(2)
.hasDays(3)Creates assertions for Path objects.
val path = Paths.get("/tmp/test.txt")
path.assert()
.exists()
.isReadable()
.isRegularFile()Creates assertions for File objects.
val file = File("/tmp/test.txt")
file.assert()
.exists()
.isFile()
.canRead()
.hasName("test.txt")Creates assertions for URL objects.
val url = URL("https://example.com")
url.assert()
.hasHost("example.com")
.hasProtocol("https")
.hasPort(443)Creates assertions for URI objects.
val uri = URI("https://example.com/path?query=value")
uri.assert()
.hasHost("example.com")
.hasPath("/path")
.hasQuery("query=value")Creates assertions for Future objects.
val future = executor.submit(Callable { "result" })
future.assert()
.isDone()
.isNotCancelled()Creates assertions for CompletableFuture objects.
val future = CompletableFuture.completedFuture("success")
future.assert()
.isCompleted()
.isCompletedWithValue("success")Creates assertions for CompletionStage objects.
val stage = CompletableFuture.completedFuture("result")
stage.assert()
.isCompleted()
.isCompletedWithValue("result")Creates assertions for Predicate functions.
val isEven = Predicate<Int> { it % 2 == 0 }
isEven.assert()
.accepts(2, 4, 6)
.rejects(1, 3, 5)Creates assertions for Throwable objects.
val exception = RuntimeException("test error")
exception.assert()
.hasMessage("test error")
.isInstanceOf(RuntimeException::class.java)Asserts that code throws a specific exception type.
assertThrownBy(IllegalArgumentException::class.java) {
throw IllegalArgumentException("invalid argument")
}.assert().hasMessage("invalid argument")Asserts that code throws a specific exception type (Kotlin reified version).
assertThrownBy<IllegalArgumentException> {
throw IllegalArgumentException("invalid argument")
}.assert().hasMessage("invalid argument")FluentAssert is built on top of AssertJ and provides additional benefits:
| Feature | AssertJ | FluentAssert |
|---|---|---|
| Syntax | assertThat(value).isEqualTo(expected) |
value.assert().isEqualTo(expected) |
| Null Safety | Manual null checks | Automatic null handling |
| Kotlin Integration | Java library | Kotlin-first design |
| Extension Functions | Not applicable | Full Kotlin extension support |
| Type Inference | Limited | Enhanced Kotlin type system |
| IDE Support | Good | Excellent (Kotlin-aware) |
- β Kotlin projects - Better integration with Kotlin idioms
- β Null-heavy code - Automatic null safety
- β Fluent style preference - More readable assertion chains
- β Type-safe assertions - Leverage Kotlin's type system
- πΈ Java projects - No need for Kotlin extensions
- πΈ Custom assertion logic - Direct AssertJ gives more control
- πΈ Performance-critical code - Minimal overhead
// User registration validation
data class User(val name: String, val age: Int, val email: String)
fun validateUser(user: User) {
user.name.assert()
.isNotBlank()
.hasSizeBetween(2, 50)
.matches("[a-zA-Z\\s]+")
user.age.assert()
.isBetween(13, 120)
user.email.assert()
.isNotBlank()
.matches("[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}")
}
// Usage
val user = User("John Doe", 25, "john@example.com")
validateUser(user)fun processData(input: List<String>): List<String> {
return input
.filter { it.isNotBlank() }
.map { it.uppercase() }
.distinct()
.sorted()
}
fun testDataProcessing() {
val input = listOf(" apple", "", "banana", " APPLE", "cherry")
val result = processData(input)
result.assert()
.hasSize(3)
.contains("APPLE", "BANANA", "CHERRY")
.isSorted()
.allMatch { it == it.uppercase() }
}suspend fun testAsyncOperations() {
val results = coroutineScope {
val deferred1 = async { fetchUser(1) }
val deferred2 = async { fetchUser(2) }
listOf(deferred1, deferred2).awaitAll()
}
results.assert()
.hasSize(2)
.allMatch { it != null }
.anySatisfy { user ->
user.name.assert().isEqualTo("John Doe")
user.id.assert().isPositive()
}
}data class DatabaseConfig(
val host: String,
val port: Int,
val database: String,
val credentials: Map<String, String>
)
fun validateConfig(config: DatabaseConfig) {
config.host.assert()
.isNotBlank()
.matches("^[a-zA-Z0-9.-]+$")
config.port.assert()
.isBetween(1024, 65535)
config.database.assert()
.isNotBlank()
.hasSizeBetween(1, 64)
config.credentials.assert()
.isNotEmpty()
.containsKey("username")
.containsKey("password")
.allSatisfy { (key, value) ->
key.assert().isNotBlank()
value.assert().isNotBlank()
}
}We welcome contributions! Please see our Contributing Guide for details.
-
Clone the repository
git clone https://github.com/Ahoo-Wang/FluentAssert.git cd FluentAssert -
Build the project
./gradlew build
-
Run tests
./gradlew test -
Run linting
./gradlew detekt
- Follow Kotlin official coding conventions
- Use 300 characters maximum line length
- Write comprehensive tests for all public APIs
- Use descriptive test method names with backticks
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Q: What is FluentAssert?
A: FluentAssert is a Kotlin library that provides fluent assertions for JDK types, making your unit tests more readable and expressive by wrapping AssertJ assertions with Kotlin extension functions.
Q: Why should I use FluentAssert instead of AssertJ directly?
A: FluentAssert provides a more Kotlin-idiomatic API with better null safety, type inference, and IDE support. The .assert() syntax is more fluent and readable than AssertJ's assertThat().
Q: Is FluentAssert production-ready?
A: Yes, FluentAssert is stable and ready for production use. It has comprehensive test coverage and follows semantic versioning.
Q: Does FluentAssert add runtime overhead?
A: Minimal. The extension functions are inlined where possible, and the library simply delegates to AssertJ, which is highly optimized.
Q: Can I use FluentAssert with other testing frameworks?
A: Yes, FluentAssert works with any testing framework that supports AssertJ assertions, including JUnit 5, TestNG, and Spock.
Q: How does null handling work?
A: All extension functions accept nullable types and handle null values appropriately, providing null-safe assertions without additional boilerplate.
Q: What JDK versions are supported?
A: FluentAssert supports Java 17 and higher, with full compatibility with all modern JDK types and APIs.
Q: How do I migrate from AssertJ to FluentAssert?
A: Replace assertThat(value) with value.assert(). The assertion methods remain the same.
Q: Can I mix FluentAssert and AssertJ in the same test?
A: Yes, they are fully compatible. You can use both APIs in the same codebase.
Q: Are there any limitations compared to AssertJ?
A: FluentAssert provides access to all AssertJ functionality. Some advanced AssertJ features may require direct AssertJ usage, but this is rare.
Q: I'm getting compilation errors. What should I check?
A: Ensure you're using Java 17+, Kotlin 1.8.0+, and have the correct dependencies. Check that your IDE is using the right JDK.
Q: Tests are failing with null pointer exceptions.
A: This usually means you're calling methods on null values. Use safe calls (?.) or check for null before assertions.
Q: IDE doesn't recognize the extension functions.
A: Ensure the FluentAssert dependency is properly configured and your IDE has Kotlin plugin updated.
Q: How can I contribute new assertion types?
A: See our Contributing Guide for detailed instructions on adding new JDK type support.
Q: Can I suggest new features?
A: Absolutely! Open an issue on GitHub with the "enhancement" label to discuss new features.
Q: Found a bug. How do I report it?
A: Create an issue on GitHub with detailed steps to reproduce, expected vs actual behavior, and your environment details.
FluentAssert is licensed under the Apache License 2.0.