Assertj json extension (#6039)

* Assertj json extension

* Allow chained invocations

* Rename

---------

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2024-06-27 10:42:14 -04:00 committed by GitHub
parent 90251b31ab
commit bc8d873c7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 832 additions and 0 deletions

View File

@ -0,0 +1,367 @@
package ca.uhn.test.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.SoftAssertions;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Fail.fail;
/**
* Assertj extension to ease testing json strings with few nested quoted strings
*/
public class AssertJson extends AbstractAssert<AssertJson, String> {
public AssertJson(String actual) {
super(actual, AssertJson.class);
}
public static AssertJson assertThat(String actual) {
return new AssertJson(actual);
}
public AssertJson hasPath(String thePath) {
isNotNull();
isNotEmpty(thePath);
Assertions.assertThat(isJsonObjStr(actual)).isTrue();
Map<String, Object> actualMap = getMap(actual);
Assertions.assertThat(actualMap).isNotNull();
getPathMap(thePath);
return this;
}
private AssertJson isNotEmpty(String thePath) {
Assertions.assertThat(thePath).isNotEmpty();
return this;
}
public AssertJson hasKeys(String... theKeys) {
isNotNull();
Map<String, Object> map = getMap(actual);
Assertions.assertThat(map).isNotNull();
Assertions.assertThat(
map.keySet()).containsAll(Arrays.asList(theKeys));
return this;
}
public AssertJson hasExactlyKeys(String... theKeys) {
isNotNull();
Map<String, Object> map = getMap(actual);
Assertions.assertThat(map).isNotNull();
Assertions.assertThat(
map.keySet()).hasSameElementsAs(Arrays.asList(theKeys));
return this;
}
public AssertJson hasExactlyKeysWithValues(List<String> theKeys, List<? extends Serializable> theValues) {
isNotNull();
if (!checkSizes(theKeys.size(), theValues.size())) {
return this;
}
Map<String, Object> map = getMap(actual);
Assertions.assertThat(map).isNotNull();
Assertions.assertThat(
map.keySet()).hasSameElementsAs(theKeys);
for (int i = 0; i <theKeys.size(); i++) {
hasKeyWithValue(theKeys.get(i), theValues.get(i));
}
return this;
}
public AssertJson hasKeyWithValue(String theKey, Object theExpectedValue) {
isNotNull();
Map<String, Object> actualMap = getMap(actual);
Assertions.assertThat(actualMap).isNotNull();
Object actualValue = actualMap.get(theKey);
JsonTestTypes actualValueType = getType(actualValue);
JsonTestTypes expectedValueType = getType(theExpectedValue);
if (actualValueType != expectedValueType) {
fail(getDifferentTypesMessage(theKey, actualValueType, expectedValueType));
}
if (isJsonObjStr(theExpectedValue)) {
assertJsonObject(actualMap, theKey, theExpectedValue);
return this;
}
if (isJsonList(theExpectedValue)) {
assertJsonList(actualMap, theKey, theExpectedValue);
return this;
}
Assertions.assertThat(actualMap)
.extracting(theKey)
.isEqualTo(theExpectedValue);
return this;
}
private void assertJsonList(Map<String, Object> theActualMap, String theKey, Object theExpectedValue) {
List<?> expectedValueList = getList((String) theExpectedValue);
Assertions.assertThat(expectedValueList).isNotNull();
Assertions.assertThat(theActualMap.get(theKey)).isNotNull().isInstanceOf(Collection.class);
List<?> actualValueList = (List<?>) theActualMap.get(theKey);
Assertions.assertThat(actualValueList)
.asList()
.hasSameElementsAs(expectedValueList);
}
private JsonTestTypes getType(Object theValue) {
if (theValue instanceof Map<?, ?>) {
return JsonTestTypes.JSON_OBJECT;
}
if (isJsonObjStr(theValue)) {
getMap((String) theValue);
return JsonTestTypes.JSON_OBJECT;
}
if (isJsonList(theValue)) {
return JsonTestTypes.JSON_LIST;
}
return JsonTestTypes.STRING_NOT_JSON;
}
private String getDifferentTypesMessage(String theKey, JsonTestTypes theActualValueType, JsonTestTypes theExpectedValueType) {
return "Types mismatch. Te expected " + (theKey == null ? " " : "'" + theKey + "' ") +
"value is a " + theExpectedValueType.myDisplay +
" whereas the actual value is a " + theActualValueType.myDisplay;
}
private boolean isJsonList(Object theValue) {
return theValue instanceof Collection<?> ||
(theValue instanceof String stringValue
&& stringValue.trim().startsWith("[")
&& stringValue.trim().endsWith("]"));
}
private void assertJsonObject(Map<String, Object> theActualMap, String theKey, Object theValue) {
Map<String, Object> expectedValueMap = getMap((String) theValue);
Assertions.assertThat(expectedValueMap).isNotNull();
Assertions.assertThat(theActualMap.get(theKey)).isNotNull().isInstanceOf(Map.class);
@SuppressWarnings("unchecked")
Map<String, Object> actualValueMap = (Map<String, Object>) theActualMap.get(theKey);
SoftAssertions lazyly = new SoftAssertions();
for (String key : actualValueMap.keySet()) {
lazyly.assertThat(actualValueMap)
.as("Unexpected value for key: " + key + ":")
.extracting(key).isEqualTo(expectedValueMap.get(key));
}
lazyly.assertAll();
}
private boolean isJsonObjStr(Object theValue) {
if (theValue instanceof String strValue) {
String trimmed = trimAll(strValue);
return trimmed.startsWith("{") && trimmed.endsWith("}") && isValidJson(trimmed);
}
return false;
}
private String trimAll(String theString) {
return theString.trim().replace("\n", "").replace("\t", "");
}
private boolean isValidJson(String theStrValue) {
getMap(theStrValue);
return true;
}
public AssertJson hasKeysWithValues(List<String> theKeys, List<Object> theValues) {
isNotNull();
checkSizes(theKeys.size(), theValues.size());
Map<String, Object> map = getMap(actual);
Assertions.assertThat(map).isNotNull();
Assertions.assertThat(map.keySet()).containsAll(theKeys);
checkKeysAndValues(map, theKeys, theValues);
return this;
}
private void checkKeysAndValues(Map<String, Object> theExpected, List<String> theKeys, List<Object> theValues) {
SoftAssertions lazyly = new SoftAssertions();
for (int i = 0; i < theKeys.size(); i++) {
lazyly.assertThat(theExpected)
.as("Unexpected value for key: " + theKeys.get(i) + ":")
.extracting(theKeys.get(i)).isEqualTo(theValues.get(i));
}
lazyly.assertAll();
}
private boolean checkSizes(int keysSize, int valuesSize) {
if (keysSize != valuesSize) {
fail("Keys and values should have same size. Received " + keysSize + " keys and " + valuesSize + " values.");
return false;
}
return true;
}
@Nonnull
private static Map<String, Object> getMap(String theJsonString) {
try {
return new ObjectMapper()
.readValue(new ByteArrayInputStream(theJsonString.getBytes()), new TypeReference<>() {});
} catch (IOException theE) {
fail("IOException: " + theE);
}
return Collections.emptyMap();
}
private List<?> getList(String theJsonString) {
try {
return new ObjectMapper()
.readValue(new ByteArrayInputStream(theJsonString.getBytes()), new TypeReference<>() {});
} catch (IOException theE) {
fail("IOException: " + theE);
}
return Collections.emptyList();
}
public AssertJson hasPaths(String... thePaths) {
for (String path : thePaths) {
hasPath(path);
}
return this;
}
public AssertJson hasPathWithValue(String thePath, String theValue) {
String[] pathElements = thePath.split("\\.");
if (pathElements.length == 1) {
hasKeyWithValue(thePath, theValue);
}
Map<String, Object> pathMap = getPathMap(thePath);
String lastPathElement = pathElements[pathElements.length - 1];
if (isJsonObjStr(theValue)) {
Assertions.assertThat(pathMap)
.extracting(lastPathElement)
.isEqualTo(getMap(theValue));
return this;
}
if (isJsonList(theValue)) {
Assertions.assertThat(pathMap)
.extracting(lastPathElement)
.asList()
.hasSameElementsAs(getList(theValue));
return this;
}
// check last path element's value
Assertions.assertThat(pathMap)
.extracting(pathElements[pathElements.length-1])
.isEqualTo(theValue);
return this;
}
public AssertJson hasPathsWithValues(List<String> thePaths, List<String> theValues) {
if (thePaths.size() != theValues.size()) {
fail("Paths size (" + thePaths.size() + ") is different than values size (" + theValues.size() + ")");
return this;
}
for (int i = 0; i < thePaths.size(); i++) {
hasPathWithValue(thePaths.get(i), theValues.get(i));
}
return this;
}
private Map<String, Object> getPathMap(String thePath) {
String[] pathElements = thePath.split("\\.");
StringBuilder pathSoFar = new StringBuilder();
Map<String, Object> pathMap = getMap(actual);
for (int i = 0; i < pathElements.length-1; i++) {
String pathElement = pathElements[i];
pathSoFar.append(StringUtils.isNotEmpty(pathSoFar) ? "." + pathElement : pathElement);
Object pathValue = pathMap.get(pathElement);
// all path values, other than the last, must be json objects (maps)
assertIsJsonObject(pathSoFar.toString(), pathValue);
@SuppressWarnings("unchecked")
Map<String, Object> aMap = (Map<String, Object>) pathValue;
pathMap = aMap;
}
return pathMap;
}
private void assertIsJsonObject(String thePath, Object theValue) {
if (theValue instanceof Map<?, ?>) {
return;
}
if (theValue instanceof String stringValue) {
if (!isJsonObjStr(theValue)) {
fail(thePath + " doesn't contain a json object but a plain string");
return;
}
try {
getMap(stringValue);
} catch (Exception theE) {
fail(thePath + " doesn't contain a json object");
}
return;
}
String msg = "Path: " + thePath + "' is not a json object but a Json list";
if (isJsonList(theValue)) {
Assertions.assertThat(theValue)
.as(msg)
.isInstanceOf(Map.class);
}
}
enum JsonTestTypes {
STRING_NOT_JSON("plain string (not json)"),
JSON_OBJECT("json object"),
JSON_LIST("json list"),
JSON_STRING("json string");
final String myDisplay;
JsonTestTypes(String theDisplay) {
myDisplay = theDisplay;
}
}
}

View File

@ -0,0 +1,465 @@
package ca.uhn.fhir.test.utilities;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
import static ca.uhn.test.util.AssertJson.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class AssertJsonTest {
private final String myJsonString = """
{
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
}
""";
String goodAddress = """
{
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": 10021
}
""";
String wrongAddress = """
{
"streetAddress": "432 Hillcrest Rd",
"city": "Mississauga",
"state": "ON",
"postalCode": 10021
}
""";
String goodPhoneNumbers = """
[
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "fax",
"number": "646 555-4567"
}
]
""";
String wrongPhoneNumbers = """
[
{
"type": "cell",
"number": "212 555-9876"
},
{
"type": "fax",
"number": "646 666-4567"
}
]
""";
@Test
void testFluency() {
assertThat(myJsonString)
.hasPath("address.city")
.hasPaths("firstName", "address.city")
.hasKeys("firstName", "address")
.hasExactlyKeys("firstName", "lastName", "age", "address", "phoneNumbers")
.hasKeyWithValue("lastName", "Smith")
.hasKeysWithValues(
List.of("firstName", "lastName"),
List.of("John", "Smith"))
.hasExactlyKeysWithValues(
List.of("firstName", "lastName", "age", "address", "phoneNumbers"),
List.of("John", "Smith", 25, goodAddress, goodPhoneNumbers))
.hasPath("lastName");
}
@Nested
class TestHasPath {
@Test
void test_success() {
assertThat(myJsonString).hasPath("address.city");
}
@Test
void test_fails_pathNotFound() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPath("address.city.neighbourhood"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("address.city doesn't contain a json object but a plain string");
}
}
@Nested
class TestHasPaths {
@Test
void test_success() {
assertThat(myJsonString).hasPaths("lastName", "address.city");
}
@Test
void test_fails_pathNotFound() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPaths("lastName", "address.city.neighbourhood"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("address.city doesn't contain a json object but a plain string");
}
}
@Nested
class TestHasPathWithValue {
@Test
void testStringValue_success() {
assertThat(myJsonString).hasPathWithValue("address.city", "New York");
}
@Test
void testExpectedStringValue_failure_isList() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue(
"phoneNumbers.number",
"212 555-1234"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Path: phoneNumbers' is not a json object but a Json list");
}
@Test
void testExpectedStringValue_failure_isObject() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue(
"address",
"432 Hillcrest Rd"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Types mismatch. Te expected 'address' value is a plain string (not json) whereas the actual value is a json object");
}
@Test
void testExpectedStringValue_failure_differentString() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue(
"address.streetAddress",
"181 Hillcrest Rd."))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("expected: \"181 Hillcrest Rd.\"")
.hasMessageContaining("but was: \"21 2nd Street\"");
}
@Test
void testExpectedObjectValue_success() {
assertThat(myJsonString).hasPathWithValue("address", goodAddress);
}
@Test
void testExpectedObjectValue_failure_isString() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("age", goodAddress))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Types mismatch. Te expected 'age' value is a json object whereas the actual value is a plain string (not json)");
}
@Test
void testExpectedObjectValue_failure_isList() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("phoneNumbers", "223-217-5555"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Types mismatch. Te expected 'phoneNumbers' value is a plain string (not json) whereas the actual value is a json list");
}
@Test
void testExpectedObjectValue_failure_different() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("address", wrongAddress))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
Multiple Failures (3 failures)
-- failure 1 --
[Unexpected value for key: streetAddress:]\s
expected: "432 Hillcrest Rd"
but was: "21 2nd Street"
""")
.hasMessageContaining("""
-- failure 2 --
[Unexpected value for key: city:]\s
expected: "Mississauga"
but was: "New York"
""")
.hasMessageContaining("""
-- failure 3 --
[Unexpected value for key: state:]\s
expected: "ON"
but was: "NY"
""");
}
@Test
void testExpectedListValue_success() {
assertThat(myJsonString).hasPathWithValue("phoneNumbers", goodPhoneNumbers);
}
@Test
void testExpectedListValue_failure_isString() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("phoneNumbers", "222-555-7654"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Types mismatch. Te expected 'phoneNumbers' value is a plain string (not json) whereas the actual value is a json list");
}
@Test
void testExpectedListValue_failure_isObject() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("phoneNumbers", goodAddress))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Types mismatch. Te expected 'phoneNumbers' value is a json object whereas the actual value is a json list");
}
@Test
void testExpectedListValue_failure_different() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathWithValue("phoneNumbers", wrongPhoneNumbers))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Expecting ArrayList:")
.hasMessageContaining("""
element(s) not found:
[{"number"="212 555-9876", "type"="cell"},
{"number"="646 666-4567", "type"="fax"}]
and element(s) not expected:
[{"number"="212 555-1234", "type"="home"},
{"number"="646 555-4567", "type"="fax"}]
""");
}
}
@Nested
class TestHasPathsWithValues {
@Test
void testMixedValues_success() {
assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(goodPhoneNumbers, goodAddress, "Smith"));
}
@Test
void testDifferentSizes_failure() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers"), List.of(goodPhoneNumbers, wrongPhoneNumbers)))
.isInstanceOf(AssertionError.class)
.hasMessageStartingWith("Paths size (1) is different than values size (2)");
}
@Test
void testMixedValues_failure_wrongTypeString() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(goodPhoneNumbers, goodAddress, wrongAddress)))
.isInstanceOf(AssertionError.class)
.hasMessage("Types mismatch. Te expected 'lastName' value is a json object whereas the actual value is a plain string (not json)");
}
@Test
void testMixedValues_failure_wrongTypeList() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of("Wesson", goodAddress, "Smith")))
.isInstanceOf(AssertionError.class)
.hasMessage("Types mismatch. Te expected 'phoneNumbers' value is a plain string (not json) whereas the actual value is a json list");
}
@Test
void testMixedValues_failure_wrongTypeObject() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(goodPhoneNumbers, "Wesson", "Smith")))
.isInstanceOf(AssertionError.class)
.hasMessage("Types mismatch. Te expected 'address' value is a plain string (not json) whereas the actual value is a json object");
}
@Test
void testMixedValues_failure_differentString() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(goodPhoneNumbers, goodAddress, "Wesson")))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("[Extracted: lastName]")
.hasMessageContaining("expected: \"Wesson\"")
.hasMessageContaining("but was: \"Smith\"");
}
@Test
void testMixedValues_failure_differentList() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(wrongPhoneNumbers, goodAddress, "Smith")))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
element(s) not found:
[{"number"="212 555-9876", "type"="cell"},
{"number"="646 666-4567", "type"="fax"}]
and element(s) not expected:
[{"number"="212 555-1234", "type"="home"},
{"number"="646 555-4567", "type"="fax"}]
""");
}
@Test
void testMixedValues_failure_differentObject() {
assertThatThrownBy(() -> assertThat(myJsonString).hasPathsWithValues(List.of("phoneNumbers", "address", "lastName"),
List.of(goodPhoneNumbers, wrongAddress, "Smith")))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
Multiple Failures (3 failures)
-- failure 1 --
[Unexpected value for key: streetAddress:]\s
expected: "432 Hillcrest Rd"
but was: "21 2nd Street"
""")
.hasMessageContaining("""
-- failure 2 --
[Unexpected value for key: city:]\s
expected: "Mississauga"
but was: "New York"
""")
.hasMessageContaining("""
-- failure 3 --
[Unexpected value for key: state:]\s
expected: "ON"
but was: "NY"
""");
}
}
@Test
void hasExactlyKeys() {
assertThat(myJsonString).hasExactlyKeys("firstName", "lastName", "age", "address", "phoneNumbers");
}
@Nested
class TestHasExactlyKeys {
@Test
void hasExactlyKeys_succeeds() {
assertThat(myJsonString).hasExactlyKeys("firstName", "lastName", "age", "address", "phoneNumbers");
}
@Test
void hasExactlyKeys_fails() {
assertThatThrownBy(() -> assertThat(myJsonString)
.hasExactlyKeys("firstName", "age", "address", "phoneNumbers", "extraKey"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
element(s) not found:
["extraKey"]""")
.hasMessageContaining("""
and element(s) not expected:
["lastName"]""");
}
}
@Nested
class TestHasKeysWithValues {
@Test
void hasExactlyKeysWithValues_succeeds() {
assertThat(myJsonString).hasKeysWithValues(
List.of("firstName", "lastName", "age"),
List.of("John", "Smith", 25));
}
@Test
void testKeysAndValues_haveDifferentSizes_fails() {
assertThatThrownBy(() ->
assertThat(myJsonString).hasKeysWithValues(
List.of("firstName", "lastName", "age"),
List.of("John", "Wesson", 31, 28)))
.isInstanceOf(AssertionError.class)
.hasMessage("Keys and values should have same size. Received 3 keys and 4 values.");
}
@Test
void testKeysAndValues_haveDifferentValues_failsShowingAllProblems() {
assertThatThrownBy(() ->
assertThat(myJsonString).hasKeysWithValues(
List.of("firstName", "lastName", "age"),
List.of("John", "Wesson", 31)))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
Multiple Failures (2 failures)
-- failure 1 --
[Unexpected value for key: lastName:]\s
expected: "Wesson"
but was: "Smith"
""")
.hasMessageContaining("""
-- failure 2 --
[Unexpected value for key: age:]\s
expected: 31
but was: 25
""");
}
}
@Nested
class TestHasKeys {
@Test
void testSuccess() {
assertThat(myJsonString).hasKeys("address", "phoneNumbers");
}
@Test
void testFailure() {
assertThatThrownBy(() -> assertThat(myJsonString).hasKeys("address", "weight"))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
but could not find the following element(s):
["weight"]
""");
}
}
@Nested
class TestHasKeyWithValue {
@Test
void testSuccess() {
assertThat(myJsonString).hasKeyWithValue("address", goodAddress);
}
@Test
void testFailure() {
assertThatThrownBy(() -> assertThat(myJsonString).hasKeyWithValue("address", wrongAddress))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("""
Multiple Failures (3 failures)
-- failure 1 --
[Unexpected value for key: streetAddress:]\s
expected: "432 Hillcrest Rd"
but was: "21 2nd Street"
""")
.hasMessageContaining("""
-- failure 2 --
[Unexpected value for key: city:]\s
expected: "Mississauga"
but was: "New York"
""")
.hasMessageContaining("""
-- failure 3 --
[Unexpected value for key: state:]\s
expected: "ON"
but was: "NY"
""");
}
}
}