From 09d63446b6ec5b55d0833ec494b2af024be8a524 Mon Sep 17 00:00:00 2001 From: kpavlov <1517853+kpavlov@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:13:36 +0300 Subject: [PATCH] [kotlin.test] Add fluent assertions DSL --- .../kotlin/kotlin/test/FluentAssertions.kt | 277 +++++++++++++++ .../kotlin/test/tests/BasicAssertionsTest.kt | 3 +- .../kotlin/test/tests/FluentAssertionsTest.kt | 330 ++++++++++++++++++ 3 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 libraries/kotlin.test/common/src/main/kotlin/kotlin/test/FluentAssertions.kt create mode 100644 libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/FluentAssertionsTest.kt diff --git a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/FluentAssertions.kt b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/FluentAssertions.kt new file mode 100644 index 0000000000000..fa02297f75cea --- /dev/null +++ b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/FluentAssertions.kt @@ -0,0 +1,277 @@ +/* + * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * Asserts that this value is equal to the [expected] value. + * + * @param expected the expected value + * @throws AssertionError if this value is not equal to the expected value + */ +@SinceKotlin("2.2.30") +public infix fun Any.shouldBe(expected: Any) { + assertEquals(expected, this) +} + +/** + * Asserts that this value is not equal to the [expected] value. + * + * @param expected the value that this should not be equal to + * @throws AssertionError if this value is equal to the expected value + */ +@SinceKotlin("2.2") +public infix fun Any.shouldNotBe(expected: Any) { + assertNotEquals(expected, this) +} + +@SinceKotlin("2.2") +@Deprecated( + message = "Use assertEquals(...) for Doubles with absolute tolerance", + replaceWith = ReplaceWith("assertEquals") +) +public infix fun Double.shouldBe(expected: Double) { + throw UnsupportedOperationException("Use assertEquals(...) for Doubles with absolute tolerance") +} + +@SinceKotlin("2.2") +@Deprecated( + message = "Use assertNotEquals(...) for Doubles with absolute tolerance", + replaceWith = ReplaceWith("assertNotEquals") +) +public infix fun Double.shouldNotBe(expected: Double) { + throw UnsupportedOperationException("Use assertNotEquals(...) for Doubles with absolute tolerance") +} + +@SinceKotlin("2.2") +@Deprecated( + message = "Use assertEquals(...) for Float with absolute tolerance", + replaceWith = ReplaceWith("assertEquals") +) +public infix fun Float.shouldBe(expected: Float) { + throw UnsupportedOperationException("Use assertNotEquals(...) for Doubles with absolute tolerance") +} + +@SinceKotlin("2.2") +@Deprecated( + message = "Use assertNotEquals(...) for Float with absolute tolerance", + replaceWith = ReplaceWith("assertNotEquals") +) +public infix fun Float.shouldNotBe(expected: Float) { + throw UnsupportedOperationException("Use assertNotEquals(...) for Float with absolute tolerance") +} + +/** + * Asserts that this value is the same instance as the [expected] value. + * + * @param expected the expected instance + * @throws AssertionError if this value is not the same instance as the expected value + */ +@SinceKotlin("2.2") +public infix fun Any.shouldBeSameAs(expected: Any) { + assertSame(expected, this) +} + +/** + * Asserts that this value is not the same instance as the [expected] value. + * + * @param expected the value that this should not be the same instance as + * @throws AssertionError if this value is the same instance as the expected value + */ +@SinceKotlin("2.2") +public infix fun Any.shouldNotBeSameAs(expected: Any) { + assertNotSame(expected, this) +} + +/** + * Asserts that this sequence contains the [expected] element. + * + * @param expected the element that should be contained in this sequence + * @throws AssertionError if this sequence does not contain the expected element + */ +@SinceKotlin("2.2") +public infix fun Sequence.shouldContain(expected: T?) { + assertContains(this, expected) +} + +/** + * Asserts that this iterable contains the [expected] element. + * + * @param expected the element that should be contained in this iterable + * @throws AssertionError if this iterable does not contain the expected element + */ +@SinceKotlin("2.2") +public infix fun Iterable.shouldContain(expected: T?) { + assertContains(this, expected) +} + +/** + * Asserts that this character sequence contains the [expected] subsequence. + * + * @param expected the subsequence that should be contained in this character sequence + * @throws AssertionError if this character sequence does not contain the expected subsequence + */ +@SinceKotlin("2.2") +public infix fun CharSequence.shouldContain(expected: CharSequence) { + assertContains(this, expected) +} + +/** + * Executes the given [block] and adds a custom message to any thrown [AssertionError]. + * The message is computed lazily by [lazyMessage] only if an error occurs. + * + * @param lazyMessage a lambda that returns a custom message to be prepended to assertion failures, or null + * @param block the code block to execute + * @throws AssertionError if the block throws an AssertionError, with the custom message prepended + */ +@SinceKotlin("2.2") +public fun withClue(lazyMessage: () -> String? = { null }, block: () -> Any) { + try { + block.invoke() + } catch (e: Throwable) { + val message = lazyMessage() + if (message != null) { + throw AssertionError("$message. ${e.message}", e) + } else if (e is AssertionError) { + throw e + } else { + throw AssertionError(e) + } + } +} + +/** + * Executes the given [block] and adds a custom [message] to any thrown [AssertionError]. + * + * @param message a custom message to be prepended to assertion failures, or null + * @param block the code block to execute + * @throws AssertionError if the block throws an AssertionError, with the custom message prepended + */ +@SinceKotlin("2.2") +public fun withClue(message: String? = null, block: () -> Unit) { + withClue(lazyMessage = { message }, block = block) +} + +/** + * Asserts that this character sequence starts with the [expected] subsequence. + * + * @param expected the subsequence that this character sequence should start with + * @throws AssertionError if this character sequence does not start with the expected subsequence + */ +@SinceKotlin("2.2") +public infix fun CharSequence.shouldStartWith(expected: CharSequence) { + assertTrue(this.startsWith(expected), "Expected <$this> to start with <$expected>.") +} + +/** + * Asserts that this character sequence ends with the [expected] subsequence. + * + * @param expected the subsequence that this character sequence should end with + * @throws AssertionError if this character sequence does not end with the expected subsequence + */ +@SinceKotlin("2.2") +public infix fun CharSequence.shouldEndWith(expected: CharSequence) { + assertTrue(this.endsWith(expected), "Expected <$this> to end with <$expected>.") +} + + +/** + * Asserts that this iterable starts with the [expected] element. + * + * @param expected the element that this iterable should start with + * @throws AssertionError if this iterable does not start with the expected element + */ +@Suppress("ReplaceAssertBooleanWithAssertEquality") +@SinceKotlin("2.2") +public infix fun Iterable.shouldStartWith(expected: T) { + assertTrue(expected == this.firstOrNull(), "Expected <$this> to start with <$expected>.") +} + +/** + * Asserts that this iterable starts with the [expected] elements in the same order. + * + * @param expected the elements that this iterable should start with + * @throws AssertionError if this iterable does not start with the expected elements + */ +@SinceKotlin("2.2") +public fun Iterable.shouldStartWith(vararg expected: T) { + val thisList = this.toList() + if (thisList.size < expected.size) { + fail("Expected <$this> to start with <${expected.contentToString()}>, but actual size ${thisList.size} is less than expected size ${expected.size}.") + } + for (i in expected.indices) { + if (thisList[i] != expected[i]) { + fail("Expected <$this> to start with <${expected.contentToString()}>, but differs at index $i: expected <${expected[i]}>, actual <${thisList[i]}>.") + } + } +} + +/** + * Asserts that this iterable ends with the [expected] elements in the same order. + * + * @param expected the elements that this iterable should end with + * @throws AssertionError if this iterable does not end with the expected elements + */ +@SinceKotlin("2.2") +public fun Iterable.shouldEndWith(vararg expected: T) { + val thisList = this.toList() + if (thisList.size < expected.size) { + fail("Expected <$this> to end with <${expected.contentToString()}>, but actual size ${thisList.size} is less than expected size ${expected.size}.") + } + val offset = thisList.size - expected.size + for (i in expected.indices) { + if (thisList[offset + i] != expected[i]) { + fail("Expected <$this> to end with <${expected.contentToString()}>, but differs at index ${offset + i}: expected <${expected[i]}>, actual <${thisList[offset + i]}>.") + } + } +} + +/** + * Asserts that this iterable ends with the [expected] element. + * + * @param expected the element that this iterable should end with + * @throws AssertionError if this iterable does not end with the expected element + */ +@Suppress("ReplaceAssertBooleanWithAssertEquality") +@SinceKotlin("2.2") +public infix fun Iterable.shouldEndWith(expected: T) { + assertTrue(expected == this.lastOrNull(), "Expected <$this> to end with <$expected>.") +} + +public fun main() { + + "Hello" shouldBe "Hello" + println("✅ 1") + + withClue({ "Should be equal" }) { + "Hello" shouldBe "Hello" + } + println("✅ 2") + + val err = assertFailsWith { + withClue("Should fail") { + "Hello" shouldBe "Goodbye" + } + } + assertEquals("Should fail. Expected , actual .", err.message) + println("✅ 3, ${err.message}") + + val error = assertFailsWith { + withClue({ "Should fail" }) { + "Hello" shouldBe "Goodbye" + } + } + assertEquals("Should fail. Expected , actual .", error.message) + println("✅ 4") + + assertFailsWith("shouldBe should fail when expected") { + "Hello" shouldBe "Goodbye" + } + + println("✅ 5") + + "Hello" shouldNotBe 1 + println("✅ 6") +} diff --git a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt index 8e2ff76ab861d..4db8ac875baec 100644 --- a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt +++ b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt @@ -357,9 +357,10 @@ class BasicAssertionsTest { } -internal fun testFailureMessage(expected: String, block: () -> Unit) { +internal fun testFailureMessage(expected: String, block: () -> Unit): AssertionError { val exception = checkFailedAssertion(block) assertEquals(expected, exception.message, "Wrong assertion message") + return exception } internal fun checkFailedAssertion(assertion: () -> Unit): AssertionError { diff --git a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/FluentAssertionsTest.kt b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/FluentAssertionsTest.kt new file mode 100644 index 0000000000000..17c7576930521 --- /dev/null +++ b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/FluentAssertionsTest.kt @@ -0,0 +1,330 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test.tests + +import kotlin.test.* +import kotlin.test.tests.testFailureMessage + +class FluentAssertionsTest { + @Test + fun testShouldBe() { + 1 shouldBe 1 + } + + @Test + fun testShouldBeFailure() { + testFailureMessage("Expected <2>, actual <1>.") { + 1 shouldBe 2 + } + } + + @Suppress("DEPRECATION") + @Test + fun testShouldBeForDoubleNotAllowed() { + assertFailsWith { + 0.01 shouldBe 0.01 + } + } + + @Suppress("DEPRECATION") + @Test + fun testShouldNotBeForDoubleNotAllowed() { + assertFailsWith { + 0.01 shouldNotBe 0.01 + } + } + + @Suppress("DEPRECATION") + @Test + fun testShouldBeForFloatNotAllowed() { + assertFailsWith { + 0.01 shouldBe 0.01 + } + } + + @Suppress("DEPRECATION") + @Test + fun testShouldNotBeForFloatNotAllowed() { + assertFailsWith { + 0.01 shouldNotBe 0.01 + } + } + + @Test + fun testShouldBeSameAs() { + val instance: Any = object {} + instance shouldBeSameAs instance + } + + @Test + fun testShouldBeSameAsFailure() { + val one: Any = object { + override fun toString() = "One" + } + val another: Any = object { + override fun toString() = "Another" + } + testFailureMessage("Expected , actual is not same.") { + one shouldBeSameAs another + } + } + + @Test + fun testShouldBeString() { + "Friends" shouldBe "Friends" + } + + @Test + fun testShouldBeStringFailure() { + testFailureMessage("Expected , actual .") { + "Friends" shouldBe "Rivals" + } + } + + @Test + fun testShouldNotBeString() { + "Friends" shouldNotBe "Rivals" + } + + @Test + fun testShouldNotBeStringFailure() { + testFailureMessage("Illegal value: .") { + "Friends" shouldNotBe "Friends" + } + } + + @Test + fun testShouldNotBe() { + 1 shouldNotBe 2 + } + + @Test + fun testShouldNotBeFailure() { + testFailureMessage("Illegal value: <1>.") { + 1 shouldNotBe 1 + } + } + + @Test + fun testShouldNotBeSameAs() { + val one: Any = object {} + val another: Any = object {} + one shouldNotBeSameAs another + } + + @Test + fun testShouldNotBeSameAsFailure() { + val instance: Any = object { + override fun toString() = "Instance" + } + testFailureMessage("Expected not same as .") { + instance shouldNotBeSameAs instance + } + } + + @Test + fun testShouldContainIterable() { + val list = listOf(1, 2, 3) + list shouldContain 2 + } + + @Test + fun testShouldContainIterableFailure() { + val list = listOf(1, 2, 3) + testFailureMessage("Expected the collection to contain the element.\nCollection <$list>, element <4>.") { + list shouldContain 4 + } + } + + @Test + fun testShouldContainSequence() { + val sequence = sequenceOf(1, 2, 3) + sequence shouldContain 2 + } + + @Test + fun testShouldContainSequenceFailure() { + val sequence = sequenceOf(1, 2, 3) + testFailureMessage("Expected the sequence to contain the element.\nSequence <$sequence>, element <4>.") { + sequence shouldContain 4 + } + } + + @Test + fun testShouldContainCharSequence() { + val text = "Hello World" + text shouldContain "World" + } + + @Test + fun testShouldContainCharSequenceFailure() { + val text = "Hello World" + testFailureMessage("Expected the char sequence to contain the substring.\nCharSequence <$text>, substring , ignoreCase .") { + text shouldContain "Goodbye" + } + } + + @Test + fun testWithClueString() { + withClue("Test message") { + 1 shouldBe 1 + } + } + + @Test + fun testWithClueStringFailure() { + testFailureMessage("Test message. Expected <2>, actual <1>.") { + withClue("Test message") { + 1 shouldBe 2 + } + } + } + + @Test + fun testWithClueStringNull() { + testFailureMessage("Test message. Expected <2>, actual <1>.") { + withClue("Test message") { + withClue(null as String?) { + 1 shouldBe 2 + } + } + } + } + + @Test + fun testWithClueLazy() { + withClue({ "Lazy message" }) { + 1 shouldBe 1 + } + } + + @Test + fun testWithClueLazyFailure() { + testFailureMessage("Lazy message. Expected <2>, actual <1>.") { + withClue({ "Lazy message" }) { + 1 shouldBe 2 + } + } + } + + @Test + fun testWithClueLazyNull() { + withClue({ null }) { + 1 shouldBe 1 + } + } + + @Test + fun testWithClueLazyNullFailure() { + testFailureMessage("Expected <2>, actual <1>.") { + withClue({ null }) { + 1 shouldBe 2 + } + } + } + + @Test + fun testWithClueNonAssertionError() { + val error = testFailureMessage("Custom message. Original error") { + withClue("Custom message") { + throw IllegalStateException("Original error") + } + } + assertTrue(error.cause is IllegalStateException) + } + + @Test + fun testShouldStartWithCharSequence() { + "Hello World" shouldStartWith "Hello" + } + + @Test + fun testShouldStartWithCharSequenceFailure() { + testFailureMessage("Expected to start with .") { + "Hello World" shouldStartWith "Goodbye" + } + } + + @Test + fun testShouldEndWithCharSequence() { + "Hello World" shouldEndWith "World" + } + + @Test + fun testShouldEndWithCharSequenceFailure() { + testFailureMessage("Expected to end with .") { + "Hello World" shouldEndWith "Goodbye" + } + } + + @Test + fun testShouldStartWithIterable() { + listOf(1, 2, 3, 4).shouldStartWith(1, 2) + } + + @Test + fun testShouldStartWithElement() { + listOf(1, 2, 3, 4).shouldStartWith(1) + } + + @Test + fun testShouldStartWithIterableFailure() { + val list = listOf(1, 2, 3, 4) + testFailureMessage("Expected <$list> to start with <[3, 4]>, but differs at index 0: expected <3>, actual <1>.") { + list.shouldStartWith(3, 4) + } + } + + @Test + fun testShouldStartWithElementFailure() { + val list = listOf(1, 2, 3, 4) + testFailureMessage("Expected <$list> to start with <3>.") { + list shouldStartWith 3 + } + } + + @Test + fun testShouldStartWithIterableSizeFailure() { + val list = listOf(1, 2) + testFailureMessage("Expected <$list> to start with <[1, 2, 3]>, but actual size 2 is less than expected size 3.") { + list.shouldStartWith(1, 2, 3) + } + } + + @Test + fun testShouldEndWithIterable() { + listOf(1, 2, 3, 4).shouldEndWith(3, 4) + } + + @Test + fun testShouldEndWithElement() { + listOf(1, 2, 3, 4) shouldEndWith 4 + } + + @Test + fun testShouldEndWithIterableFailure() { + val list = listOf(1, 2, 3, 4) + testFailureMessage("Expected <$list> to end with <[1, 2]>, but differs at index 2: expected <1>, actual <3>.") { + list.shouldEndWith(1, 2) + } + } + + @Test + fun testShouldEndWithIterableSizeFailure() { + val list = listOf(1, 2) + testFailureMessage("Expected <$list> to end with <[1, 2, 3]>, but actual size 2 is less than expected size 3.") { + list.shouldEndWith(1, 2, 3) + } + } + + @Test + fun testShouldEndWithElementFailure() { + val list = listOf(1, 2, 3, 4) + testFailureMessage("Expected <$list> to end with <1>.") { + list shouldEndWith 1 + } + } +}