Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release_base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
- publishKotestBomPublicationToDeployRepository
- publishLinuxX64PublicationToDeployRepository
- publishLinuxArm64PublicationToDeployRepository
- publishAndroidNativeX86PublicationToDeployRepository
- publishAndroidNativeX64PublicationToDeployRepository
- publishAndroidNativeArm32PublicationToDeployRepository
- publishAndroidNativeArm64PublicationToDeployRepository
steps:
- name: Checkout the repo
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release_watchos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
- "publishWatchosArm64PublicationToDeployRepository"
- "publishWatchosX64PublicationToDeployRepository"
- "publishWatchosSimulatorArm64PublicationToDeployRepository"
- "publishWatchosDeviceArm64PublicationToDeployRepository"
steps:
- name: Checkout the repo
uses: actions/checkout@v4
Expand Down
14 changes: 9 additions & 5 deletions documentation/docs/extensions/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ With *System Environment Extension* you can simulate how the System Environment
Kotest provides some extension functions that provides a System Environment in a specific scope:

```kotlin
withEnvironment("FooKey", "BarValue") {
System.getenv("FooKey") shouldBe "BarValue" // System environment overridden!
test("foo") {
withEnvironment("FooKey", "BarValue") {
System.getenv("FooKey") shouldBe "BarValue" // System environment overridden!
}
}
```

:::info
To use `withEnvironment` with JDK17 you need to add `--add-opens=java.base/java.util=ALL-UNNAMED` to the arguments for the JVM that runs the tests.
To use `withEnvironment` with JDK17+ you need to add `--add-opens=java.base/java.util=ALL-UNNAMED` to the arguments for the JVM that runs the tests.

If you run tests with gradle, you can add the following to your `build.gradle.kts`:

Expand All @@ -52,8 +54,10 @@ tasks.withType<Test>().configureEach {
You can also use multiple values in this extension, through a map or list of pairs.

```kotlin
withEnvironment(mapOf("FooKey" to "BarValue", "BarKey" to "FooValue")) {
// Use FooKey and BarKey
test("foo") {
withEnvironment(mapOf("FooKey" to "BarValue", "BarKey" to "FooValue")) {
// Use FooKey and BarKey
}
}
```

Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/extensions/test_containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `LifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class QueryDatastoreTest : FunSpec({

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::

#### Initializing the Database Container
Expand Down Expand Up @@ -169,7 +169,7 @@ as a general container.

:::tip
This extension also supports the `ContainerLifecycleMode` flag to control when the container is started and stopped.
See #lifecycle
See [Lifecycle](#lifecycle)
:::


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,47 @@ fun <T> Array<T>.shouldNotHaveElementAt(index: Int, element: T) = asList().shoul
fun <T> List<T>.shouldNotHaveElementAt(index: Int, element: T) = this shouldNot haveElementAt(index, element)

fun <T, L : List<T>> haveElementAt(index: Int, element: T) = object : Matcher<L> {
override fun test(value: L) =
MatcherResult(
index < value.size && value[index] == element,
{ "Collection ${value.print().value} should contain ${element.print().value} at index $index" },
override fun test(value: L): MatcherResult {
val passed = index < value.size && value[index] == element
val listTooShortMsg = if(index < value.size) "" else "But it is too short: only ${value.size} elements"
val unexpectedElementMsg = when {
passed -> ""
index < value.size -> "Expected: <${value[index].print().value}>, but was <${element.print().value}>"
else -> ""
}
val indexesForElement = value.mapIndexedNotNull { index, current ->
if(current == element) index else null
}
val indexesForElementMsg = if(passed || indexesForElement.isEmpty())
""
else "Element was found at index(es): ${indexesForElement.print().value}"
val additionalDescriptions = listOf(listTooShortMsg, unexpectedElementMsg, indexesForElementMsg).filter {
it.isNotEmpty()
}
val additionalDescriptionsMsg = if(additionalDescriptions.isEmpty()) ""
else "\n${additionalDescriptions.joinToString("\n")}"
return MatcherResult(
passed,
{ "Collection ${value.print().value} should contain ${element.print().value} at index $index$additionalDescriptionsMsg" },
{ "Collection ${value.print().value} should not contain ${element.print().value} at index $index" }
)
}
}

infix fun <T> Iterable<T>.shouldExist(p: (T) -> Boolean) = toList().shouldExist(p)
infix fun <T> Array<T>.shouldExist(p: (T) -> Boolean) = asList().shouldExist(p)
infix fun <T> Collection<T>.shouldExist(p: (T) -> Boolean) = this should exist(p)
fun <T> exist(p: (T) -> Boolean) = object : Matcher<Collection<T>> {
override fun test(value: Collection<T>) = MatcherResult(
value.any { p(it) },
{ "Collection ${value.print().value} should contain an element that matches the predicate $p" },
{ "Collection ${value.print().value} should not contain an element that matches the predicate $p" }
)
override fun test(value: Collection<T>): MatcherResult {
val matchingElementsIndexes = value.mapIndexedNotNull { index, element ->
if(p(element)) index else null
}
return MatcherResult(
matchingElementsIndexes.isNotEmpty(),
{ "Collection ${value.print().value} should contain an element that matches the predicate $p" },
{ "Collection ${value.print().value} should not contain an element that matches the predicate $p, but elements with the following indexes matched: ${matchingElementsIndexes.print().value}" }
)
}
}

fun <T> Iterable<T>.shouldMatchInOrder(vararg assertions: (T) -> Unit) = toList().shouldMatchInOrder(assertions.toList())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ fun <T> singleElement(t: T): Matcher<Collection<T>> = object : Matcher<Collectio
expected = t.print().value,
)
} else {
val elementFoundAtIndexes = value.mapIndexedNotNull {
index, element ->
if(element == t) index else null
}
val foundAtMessage = if(elementFoundAtIndexes.isEmpty()) "Element not found in collection"
else "Element found at index(es): ${elementFoundAtIndexes.print().value}"
MatcherResult(
passed = false,
failureMessageFn = { "Collection should be a single element of $t but has ${value.size} elements: ${value.print().value}" },
failureMessageFn = { "Collection should be a single element of $t but has ${value.size} elements: ${value.print().value}. $foundAtMessage." },
negatedFailureMessageFn = { "Collection should not be a single element of $t" },
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.kotest.matchers.collections.containDuplicates
import io.kotest.matchers.collections.containNoNulls
import io.kotest.matchers.collections.containNull
import io.kotest.matchers.collections.containOnlyNulls
import io.kotest.matchers.collections.exist
import io.kotest.matchers.collections.existInOrder
import io.kotest.matchers.collections.haveElementAt
import io.kotest.matchers.collections.haveSize
Expand Down Expand Up @@ -72,6 +73,7 @@ import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldHave
import io.kotest.matchers.shouldNot
import io.kotest.matchers.shouldNotHave
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.throwable.shouldHaveMessage

class CollectionMatchersTest : WordSpec() {
Expand Down Expand Up @@ -143,6 +145,40 @@ class CollectionMatchersTest : WordSpec() {
tests should haveElementAt(0, TestSealed.Test1("test1"))
tests.shouldHaveElementAt(1, TestSealed.Test2(2))
}
"print if list is too short" {
shouldThrowAny {
listOf("a", "b", "c").shouldHaveElementAt(3, "d")
}.message shouldBe """
|Collection ["a", "b", "c"] should contain "d" at index 3
|But it is too short: only 3 elements
""".trimMargin()
}
"print if element does not match" {
shouldThrowAny {
listOf("a", "b", "c").shouldHaveElementAt(2, "d")
}.message shouldBe """
|Collection ["a", "b", "c"] should contain "d" at index 2
|Expected: <"c">, but was <"d">
""".trimMargin()
}
"print if element found at another index" {
shouldThrowAny {
listOf("a", "b", "c").shouldHaveElementAt(2, "b")
}.message shouldBe """
|Collection ["a", "b", "c"] should contain "b" at index 2
|Expected: <"c">, but was <"b">
|Element was found at index(es): [1]
""".trimMargin()
}
"print if element found at multiple other indexes" {
shouldThrowAny {
listOf("a", "b", "c", "b").shouldHaveElementAt(2, "b")
}.message shouldBe """
|Collection ["a", "b", "c", "b"] should contain "b" at index 2
|Expected: <"c">, but was <"b">
|Element was found at index(es): [1, 3]
""".trimMargin()
}
}

"containNull()" should {
Expand Down Expand Up @@ -250,7 +286,11 @@ class CollectionMatchersTest : WordSpec() {

shouldThrow<AssertionError> {
listOf(1, 2) shouldBe singleElement(2)
}.shouldHaveMessage("Collection should be a single element of 2 but has 2 elements: [1, 2]")
}.shouldHaveMessage("Collection should be a single element of 2 but has 2 elements: [1, 2]. Element found at index(es): [1].")

shouldThrow<AssertionError> {
listOf(1, 2) shouldBe singleElement(3)
}.shouldHaveMessage("Collection should be a single element of 3 but has 2 elements: [1, 2]. Element not found in collection.")
}
}

Expand Down Expand Up @@ -490,6 +530,11 @@ class CollectionMatchersTest : WordSpec() {
val list = listOf(1, 2, 3)
list.shouldExist { it == 2 }
}
"give descriptive message when predicate should not match" {
shouldThrowAny {
listOf(1, 2, 3, 2) shouldNot exist { it == 2}
}.message shouldBe "Collection [1, 2, 3, 2] should not contain an element that matches the predicate (kotlin.Int) -> kotlin.Boolean, but elements with the following indexes matched: [1, 3]"
}
}

"shouldHaveAtLeastSize" should {
Expand Down
Loading