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

Better assertion failures for containJsonKeyValue #3949

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to print partial matches, such as:

Expected given to contain json key <'$.store.book.weight'> but key was not found. 
Found the following sub-key: <'$.store.book'>.

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose it would, but I don't believe we get that information out of the JsonPath library. How do you intend that we should calculate that subkey?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think perhaps we can merge this small improvement as-is, and we could make further improvements in a separate PR :) We can continue the discussion here even after PR is merged

Copy link
Contributor

Choose a reason for hiding this comment

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

I think perhaps we can merge this small improvement as-is, and we could make further improvements in a separate PR :) We can continue the discussion here even after PR is merged

sure - #4013

""".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