Skip to content

Commit

Permalink
Better assertion failures for containJsonKeyValue (#3949)
Browse files Browse the repository at this point in the history
Now prints whether the key was missing, or if it had wrong value

<!-- 
If this PR updates documentation, please update all relevant versions of
the docs, see:
https://github.com/kotest/kotest/tree/master/documentation/versioned_docs
The documentation at
https://github.com/kotest/kotest/tree/master/documentation/docs is the
documentation for the next minor or major version _TO BE RELEASED_
-->
  • Loading branch information
Kantis committed May 9, 2024
1 parent 3ab9358 commit de54d17
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package io.kotest.assertions.json
import com.jayway.jsonpath.InvalidPathException
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.PathNotFoundException
import io.kotest.assertions.Actual
import io.kotest.assertions.Expected
import io.kotest.assertions.intellijFormatError
import io.kotest.assertions.print.print
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.should
Expand All @@ -21,23 +25,42 @@ inline fun <reified T> String.shouldNotContainJsonKeyValue(path: String, value:
this shouldNot containJsonKeyValue(path, value)

inline fun <reified T> containJsonKeyValue(path: String, t: T) = object : Matcher<String?> {
override fun test(value: String?): MatcherResult {
val sub = when (value) {
null -> value
else -> if (value.length < 50) value.trim() else value.substring(0, 50).trim() + "..."
}
private fun keyIsAbsentFailure() = MatcherResult(
false,
{ "Expected given to contain json key <'$path'> but key was not found." },
{ "Expected given to not contain json key <'$path'> but key was found." }
)

val passed = value != null && try {
JsonPath.parse(value).read(path, T::class.java) == t
private fun invalidJsonFailure(actualJson: String?) = MatcherResult(
false,
{ "Expected a valid JSON, but was ${if (actualJson == null) "null" else "empty" }" },
{ "Expected a valid JSON, but was ${if (actualJson == null) "null" else "empty" }" },
)

private fun extractKey(value: String?): T? {
return try {
JsonPath.parse(value).read(path, T::class.java)
} catch (e: PathNotFoundException) {
false
null
} catch (e: InvalidPathException) {
throw AssertionError("$path is not a valid JSON path")
}
}

override fun test(value: String?): MatcherResult {
if (value.isNullOrEmpty()) return invalidJsonFailure(value)

val sub =
if (value.length < 50) value.trim()
else value.substring(0, 50).trim() + "..."

val actualKeyValue = extractKey(value)
val passed = t == actualKeyValue
if (!passed && actualKeyValue == null) return keyIsAbsentFailure()

return MatcherResult(
passed,
{ "$sub should contain the element $path = $t" },
{ "Value mismatch at '$path': ${intellijFormatError(Expected(t.print()), Actual(actualKeyValue.print()))}" },
{
"$sub should not contain the element $path = $t"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.sksamuel.kotest.tests.json

import io.kotest.assertions.asClue
import io.kotest.assertions.json.shouldContainJsonKeyValue
import io.kotest.assertions.json.shouldNotContainJsonKeyValue
import io.kotest.assertions.shouldFail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import org.intellij.lang.annotations.Language

class ContainJsonKeyValueTest : StringSpec({
@Language("JSON")
val json = """
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}
],
"bicycle": {
"color": "red",
"price": 19.95,
"code": 1
}
}
}
""".trimIndent()

"Negated assertions" {
"".shouldNotContainJsonKeyValue("$.store.bicycle.color", "red")
"{}".shouldNotContainJsonKeyValue("$.store.bicycle.color", "red")
"""{ "foo": "bar" }""".shouldNotContainJsonKeyValue("foo", "baz")
shouldFail {
"""{ "foo": "bar" }""".shouldNotContainJsonKeyValue("foo", "bar")
}.message shouldBe """{ "foo": "bar" } should not contain the element foo = bar"""
}

"Failure message states if key is missing, when it's missing" {
shouldFail {
json.shouldContainJsonKeyValue("$.bicycle.engine", "V2")
}.message shouldBe """
Expected given to contain json key <'$.bicycle.engine'> but key was not found.
""".trimIndent()
}

"Failure message states states value mismatch if key is present with different value" {
shouldFail {
json.shouldContainJsonKeyValue("$.store.book[0].price", 9.95)
}.message shouldBe """
Value mismatch at '$.store.book[0].price': expected:<9.95> but was:<8.95>
""".trimIndent()
}

"test json key value" {
json.shouldContainJsonKeyValue("$.store.bicycle.color", "red")
json.shouldContainJsonKeyValue("$.store.book[0].category", "reference")
json.shouldContainJsonKeyValue("$.store.book[0].price", 8.95)
json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh")
json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh")
json.shouldContainJsonKeyValue("$.store.bicycle.code", 1L)

json.shouldNotContainJsonKeyValue("$.store.book[1].author", "JK Rowling")

shouldThrow<AssertionError> { null.shouldContainJsonKeyValue("ab", "cd") }.message shouldBe
"Expected a valid JSON, but was null"

shouldThrow<AssertionError> { "".shouldContainJsonKeyValue("ab", "cd") }.message shouldBe
"Expected a valid JSON, but was empty"

"contract should work".asClue {
fun use(@Suppress("UNUSED_PARAMETER") json: String) {}

val nullableJson = """{"data": "value"}"""
nullableJson.shouldContainJsonKeyValue("data", "value")
use(nullableJson)
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -65,41 +65,6 @@ class JvmJsonAssertionsTest : StringSpec({
}
}

"test json key value" {
json.shouldContainJsonKeyValue("$.store.bicycle.color", "red")
json.shouldContainJsonKeyValue("$.store.book[0].category", "reference")
json.shouldContainJsonKeyValue("$.store.book[0].price", 8.95)
json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh")
json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh")
json.shouldContainJsonKeyValue("$.store.bicycle.code", 1L)

json.shouldNotContainJsonKeyValue("$.store.book[1].author", "JK Rowling")

shouldFail { json.shouldContainJsonKeyValue("$.store.bicycle.wheels", 2) }
.message shouldBe """{
"store": {
"book": [
{... should contain the element ${'$'}.store.bicycle.wheels = 2
""".trimIndent()

shouldThrow<AssertionError> {
json.shouldContainJsonKeyValue("$.store.book[1].author", "JK Rowling")
}.message shouldBe """{
"store": {
"book": [
{... should contain the element ${'$'}.store.book[1].author = JK Rowling"""

shouldThrow<AssertionError> { null.shouldContainJsonKeyValue("ab", "cd") }

"contract should work".asClue {
fun use(@Suppress("UNUSED_PARAMETER") json: String) {}

val nullableJson = """{"data": "value"}"""
nullableJson.shouldContainJsonKeyValue("data", "value")
use(nullableJson)
}
}

"test json match by resource" {

val testJson1 = """ { "name" : "sam", "location" : "chicago" } """
Expand Down

0 comments on commit de54d17

Please sign in to comment.