Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify property extractor function names #524

Merged
merged 11 commits into from
Apr 18, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed
- renamed `prop` to `having` as part of effort to unify API naming, old name is deprecated.
- renamed `suspendCall` to `having` as part of effort to unify API naming, old name is deprecated.

## [0.28.1] 2024-04-17

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ import assertk.assertions.support.appendName
* @param extract The suspend function to extract the property value out of the value of the current assert.
*
* ```
* assertThat(person).suspendCall("name()", { it.name() }).isEqualTo("Sue")
* assertThat(person).having("name()", { it.name() }).isEqualTo("Sue")
* ```
*/
suspend fun <T, P> Assert<T>.having(name: String, extract: suspend (T) -> P): Assert<P> =
Copy link
Contributor

@JakeWharton JakeWharton Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make the other one inline, we can eliminate this psuedo-overload. Do we want that? Both APIs used in the implementation are public.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah doing that would be fine

transform(appendName(name, separator = ".")) { extract(it) }

@Deprecated(
message = "Function suspendCall has been renamed to having",
replaceWith = ReplaceWith("having(name, extract)"),
level = DeprecationLevel.WARNING
)
suspend fun <T, P> Assert<T>.suspendCall(name: String, extract: suspend (T) -> P): Assert<P> =
transform(appendName(name, separator = ".")) { extract(it) }
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import assertk.assertions.support.expected
import assertk.assertions.support.expectedListDiff
import assertk.assertions.support.show
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*

suspend fun Assert<Flow<*>>.count(): Assert<Int> = suspendCall("count()", Flow<*>::count)
suspend fun Assert<Flow<*>>.count(): Assert<Int> = having("count()", Flow<*>::count)

/**
* Asserts the flow is empty. Fails as soon as the flow delivers an element.
Expand Down Expand Up @@ -190,4 +189,4 @@ suspend fun Assert<Flow<*>>.containsExactly(vararg elements: Any?) = given { act
}

private class AbortFlowException :
CancellationException("Flow was aborted, no more elements needed")
CancellationException("Flow was aborted, no more elements needed")
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.coroutines.assertions.suspendCall
import assertk.coroutines.assertions.having
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
Expand All @@ -14,22 +14,22 @@ class AnyTest {
val subject = BasicObject("test")

@Test
fun suspendCall_passes() = runTest {
assertThat(subject).suspendCall("str") { it.str() }.isEqualTo("test")
fun having_passes() = runTest {
assertThat(subject).having("str") { it.str() }.isEqualTo("test")
}

@Test
fun suspendCall_includes_name_in_failure_message() = runTest {
fun having_includes_name_in_failure_message() = runTest {
val error = assertFailsWith<AssertionError> {
assertThat(subject).suspendCall("str") { it.str() }.isEmpty()
assertThat(subject).having("str") { it.str() }.isEmpty()
}
assertEquals("expected [str] to be empty but was:<\"test\"> (test)", error.message)
}

@Test
fun nested_suspendCall_include_names_in_failure_message() = runTest {
fun nested_having_include_names_in_failure_message() = runTest {
val error = assertFailsWith<AssertionError> {
assertThat(subject).suspendCall("other") { it.other() }.suspendCall("str") { it?.str() }.isNotNull()
assertThat(subject).having("other") { it.other() }.having("str") { it?.str() }.isNotNull()
}
assertEquals("expected [other.str] to not be null (test)", error.message)
}
Expand Down
42 changes: 34 additions & 8 deletions assertk/src/commonMain/kotlin/assertk/assertions/any.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import kotlin.reflect.KProperty1
/**
* Returns an assert on the kotlin class of the value.
*/
fun Assert<Any>.kClass() = prop("class") { it::class }
fun Assert<Any>.kClass() = having("class") { it::class }

/**
* Returns an assert on the toString method of the value.
*/
fun Assert<Any?>.toStringFun() = prop("toString", Any?::toString)
fun Assert<Any?>.toStringFun() = having("toString", Any?::toString)

/**
* Returns an assert on the hasCode method of the value.
*/
fun Assert<Any>.hashCodeFun() = prop("hashCode", Any::hashCode)
fun Assert<Any>.hashCodeFun() = having("hashCode", Any::hashCode)

/**
* Asserts the value is equal to the expected one, using `==`.
Expand Down Expand Up @@ -146,39 +146,64 @@ fun <T : Any> Assert<T?>.isNotNull(): Assert<T> = transform { actual ->
* @param extract The function to extract the property value out of the value of the current assert.
*
* ```
* assertThat(person).prop("name", { it.name }).isEqualTo("Sue")
* assertThat(person).having("name", { it.name }).isEqualTo("Sue")
* ```
*/
fun <T, P> Assert<T>.having(name: String, extract: (T) -> P): Assert<P> =
transform(appendName(name, separator = "."), extract)

@Deprecated(
message = "Function prop has been renamed to having",
replaceWith = ReplaceWith("having(name, extract)"),
level = DeprecationLevel.WARNING
)
fun <T, P> Assert<T>.prop(name: String, extract: (T) -> P): Assert<P> =
transform(appendName(name, separator = "."), extract)


/**
* Returns an assert that asserts on the given property.
*
* Example:
* ```
* assertThat(person).prop(Person::name).isEqualTo("Sue")
* assertThat(person).having(Person::name).isEqualTo("Sue")
* ```
*
* @param property Property on which to assert. The name of this
* property will be shown in failure messages.
*/
fun <T, P> Assert<T>.having(property: KProperty1<T, P>): Assert<P> =
having(property.name) { property.get(it) }

@Deprecated(
message = "Function prop has been renamed to having",
replaceWith = ReplaceWith("having(property)"),
level = DeprecationLevel.WARNING
)
fun <T, P> Assert<T>.prop(property: KProperty1<T, P>): Assert<P> =
prop(property.name) { property.get(it) }
having(property.name) { property.get(it) }

/**
* Returns an assert that asserts on the result of calling the given function.
*
* Example:
* ```
* assertThat(person).prop(Person::nameAsLowerCase).isEqualTo("sue")
* assertThat(person).having(Person::nameAsLowerCase).isEqualTo("sue")
* ```
*
* @param callable Callable on which to assert. The name of this
* callable will be shown in the failure messages.
*/
fun <T, R, F> Assert<T>.having(callable: F): Assert<R> where F : (T) -> R, F : KCallable<R> =
having(callable.name, callable)

@Deprecated(
message = "Function prop has been renamed to having",
replaceWith = ReplaceWith("having(callable)"),
level = DeprecationLevel.WARNING
)
fun <T, R, F> Assert<T>.prop(callable: F): Assert<R> where F : (T) -> R, F : KCallable<R> =
prop(callable.name, callable)
having(callable.name, callable)

/**
* Asserts the value has the expected kotlin class. This is an exact match, so `assertThat("test").hasClass<String>()`
Expand Down Expand Up @@ -219,6 +244,7 @@ fun <T : Any> Assert<T>.doesNotHaveClass(kclass: KClass<out T>) = given { actual
if (kclass != actual::class) return
expected("to not have class:${show(kclass)}")
}

/**
* Asserts the value is not an instance of the expected kotlin class. Both
* `assertThat("test").isNotInstanceOf<String>()` and `assertThat("test").isNotInstanceOf<String>()` fail.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import assertk.assertions.support.*
/**
* Returns an assert on the Arrays's size.
*/
fun Assert<Array<*>>.size() = prop("size") { it.size }
fun Assert<Array<*>>.size() = having("size") { it.size }

/**
* Asserts the array contents are equal to the expected one, using [contentDeepEquals].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import assertk.assertions.support.show
/**
* Returns an assert on the CharSequence's length.
*/
fun Assert<CharSequence>.length() = prop("length", CharSequence::length)
fun Assert<CharSequence>.length() = having("length", CharSequence::length)

/**
* Asserts the char sequence is empty.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import assertk.assertions.support.*
/**
* Returns an assert on the Collection's size.
*/
fun Assert<Collection<*>>.size() = prop("size", Collection<*>::size)
fun Assert<Collection<*>>.size() = having("size", Collection<*>::size)

/**
* Asserts the collection is empty.
Expand Down
2 changes: 1 addition & 1 deletion assertk/src/commonMain/kotlin/assertk/assertions/map.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import assertk.assertions.support.show
/**
* Returns an assert on the Maps's size.
*/
fun Assert<Map<*, *>>.size() = prop("size", Map<*, *>::size)
fun Assert<Map<*, *>>.size() = having("size", Map<*, *>::size)

/**
* Asserts the collection is empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package assertk.assertions

import assertk.Assert
import assertk.ValueAssert
import assertk.all

/**
* Returns an assert on the throwable's message.
*/
fun Assert<Throwable>.message() = prop("message", Throwable::message)
fun Assert<Throwable>.message() = having("message", Throwable::message)

/**
* Returns an assert on the throwable's cause.
*/
fun Assert<Throwable>.cause() = prop("cause", Throwable::cause)
fun Assert<Throwable>.cause() = having("cause", Throwable::cause)

/**
* Returns an assert on the throwable's root cause.
*/
fun Assert<Throwable>.rootCause() = prop("rootCause", Throwable::rootCause)
fun Assert<Throwable>.rootCause() = having("rootCause", Throwable::rootCause)

/**
* Asserts the throwable has the expected message.
Expand Down
42 changes: 21 additions & 21 deletions assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,66 +240,66 @@ class AnyTest {
assertEquals("expected [int]:<[99]> but was:<[42]> (test)", error.message)
}

//region prop
//region having
@Test
fun prop_passes() {
assertThat(subject).prop("str") { it.str }.isEqualTo("test")
fun having_passes() {
assertThat(subject).having("str") { it.str }.isEqualTo("test")
}

@Test
fun prop_includes_name_in_failure_message() {
fun having_includes_name_in_failure_message() {
val error = assertFailsWith<AssertionError> {
assertThat(subject).prop("str") { it.str }.isEmpty()
assertThat(subject).having("str") { it.str }.isEmpty()
}
assertEquals("expected [str] to be empty but was:<\"test\"> (test)", error.message)
}

@Test
fun nested_prop_include_names_in_failure_message() {
fun nested_having_include_names_in_failure_message() {
val error = assertFailsWith<AssertionError> {
assertThat(subject).prop("other") { it.other }.prop("str") { it?.str }.isNotNull()
assertThat(subject).having("other") { it.other }.having("str") { it?.str }.isNotNull()
}
assertEquals("expected [other.str] to not be null (test)", error.message)
}

@Test
fun prop_property1_extract_prop_passes() {
assertThat(subject).prop(BasicObject::str).isEqualTo("test")
fun having_property1_extract_prop_passes() {
assertThat(subject).having(BasicObject::str).isEqualTo("test")
}

@Test
fun prop_property1_extract_prop_includes_name_in_failure_message() {
fun having_property1_extract_prop_includes_name_in_failure_message() {
val error = assertFailsWith<AssertionError> {
assertThat(subject).prop(BasicObject::str).isEmpty()
assertThat(subject).having(BasicObject::str).isEmpty()
}
assertEquals("expected [str] to be empty but was:<\"test\"> (test)", error.message)
}

@Test
fun prop_property1_includes_error_message_when_fails() {
fun having_property1_includes_error_message_when_fails() {
val error = assertFails {
assertThat(subject).prop(BasicObject::failing).isEmpty()
assertThat(subject).having(BasicObject::failing).isEmpty()
}
assertEquals("sorry!", error.message)
}

@Test
fun prop_callable_function_passes() {
assertThat(subject).prop(BasicObject::funcA).isEqualTo("A")
fun having_callable_function_passes() {
assertThat(subject).having(BasicObject::funcA).isEqualTo("A")
}

@Test
fun prop_callable_function_includes_name_in_failure_message() {
fun having_callable_function_includes_name_in_failure_message() {
val error = assertFailsWith<AssertionError> {
assertThat(subject).prop(BasicObject::funcA).isEqualTo(14)
assertThat(subject).having(BasicObject::funcA).isEqualTo(14)
}
assertEquals("expected [funcA]:<[14]> but was:<[\"A\"]> (test)", error.message)
}

@Test
fun nested_prop_callable_function_include_names_in_failure_message() {
fun nested_having_callable_function_include_names_in_failure_message() {
val error = assertFailsWith<AssertionError> {
assertThat(subject).prop(BasicObject::funcB).prop(BasicObject::funcA).isNull()
assertThat(subject).having(BasicObject::funcB).having(BasicObject::funcA).isNull()
}
assertEquals("expected [funcB.funcA] to be null but was:<\"A\"> (test)", error.message)
}
Expand Down Expand Up @@ -456,7 +456,7 @@ class AnyTest {
@Test
fun isInstanceOf_kclass_run_block_when_passes() {
val error = assertFailsWith<AssertionError> {
assertThat(subject as TestObject).isInstanceOf(BasicObject::class).prop("str", BasicObject::str)
assertThat(subject as TestObject).isInstanceOf(BasicObject::class).having("str", BasicObject::str)
.isEqualTo("wrong")
}
assertEquals("expected [str]:<\"[wrong]\"> but was:<\"[test]\"> (test)", error.message)
Expand All @@ -465,7 +465,7 @@ class AnyTest {
@Test
fun isInstanceOf_reified_kclass_run_block_when_passes() {
val error = assertFailsWith<AssertionError> {
assertThat(subject as TestObject).isInstanceOf<BasicObject>().prop("str", BasicObject::str)
assertThat(subject as TestObject).isInstanceOf<BasicObject>().having("str", BasicObject::str)
.isEqualTo("wrong")
}
assertEquals("expected [str]:<\"[wrong]\"> but was:<\"[test]\"> (test)", error.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,4 +630,4 @@ class IterableTest {
}

private fun <T> emptyIterable(): Iterable<T> = emptyList()
private fun <T> iterableOf(vararg elements: T): Iterable<T> = elements.toList()
private fun <T> iterableOf(vararg elements: T): Iterable<T> = elements.toList()
Original file line number Diff line number Diff line change
Expand Up @@ -661,4 +661,4 @@ class SequenceTest {
private fun <T> oneshotSequenceOf(vararg elements: T): Sequence<T> {
var i = 0
return generateSequence { elements.getOrNull(i++) }
}
}
6 changes: 3 additions & 3 deletions assertk/src/jvmMain/kotlin/assertk/assertions/any.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlin.reflect.full.memberProperties
/**
* Returns an assert on the java class of the value.
*/
fun <T : Any> Assert<T>.jClass() = prop("class") { it::class.java }
fun <T : Any> Assert<T>.jClass() = having("class") { it::class.java }

/**
* Asserts the value has the expected java class. This is an exact match, so
Expand Down Expand Up @@ -86,7 +86,7 @@ private fun <T> Assert<T>.isDataClassEqualToImpl(expected: T, kclass: KClass<*>?
for (memberProp in kclass.memberProperties) {
@Suppress("UNCHECKED_CAST")
val force = memberProp as KProperty1<T, Any?>
prop(force).isDataClassEqualToImpl(force.get(expected), force.returnType.classifier as? KClass<*>)
having(force).isDataClassEqualToImpl(force.get(expected), force.returnType.classifier as? KClass<*>)
}
} else {
isEqualTo(expected)
Expand Down Expand Up @@ -120,4 +120,4 @@ fun <T : Any> Assert<T>.isEqualToIgnoringGivenProperties(other: T, vararg proper
}
}
}
}
}
Loading