diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/AssertJson.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/AssertJson.java new file mode 100644 index 00000000000..e94f45614a2 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/util/AssertJson.java @@ -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 { + + 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 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 map = getMap(actual); + Assertions.assertThat(map).isNotNull(); + + Assertions.assertThat( + map.keySet()).containsAll(Arrays.asList(theKeys)); + return this; + } + + public AssertJson hasExactlyKeys(String... theKeys) { + isNotNull(); + + Map map = getMap(actual); + Assertions.assertThat(map).isNotNull(); + + Assertions.assertThat( + map.keySet()).hasSameElementsAs(Arrays.asList(theKeys)); + return this; + } + + public AssertJson hasExactlyKeysWithValues(List theKeys, List theValues) { + isNotNull(); + + if (!checkSizes(theKeys.size(), theValues.size())) { + return this; + } + + Map map = getMap(actual); + Assertions.assertThat(map).isNotNull(); + + Assertions.assertThat( + map.keySet()).hasSameElementsAs(theKeys); + + for (int i = 0; i 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 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 theActualMap, String theKey, Object theValue) { + Map expectedValueMap = getMap((String) theValue); + Assertions.assertThat(expectedValueMap).isNotNull(); + + Assertions.assertThat(theActualMap.get(theKey)).isNotNull().isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map actualValueMap = (Map) 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 theKeys, List theValues) { + isNotNull(); + + checkSizes(theKeys.size(), theValues.size()); + + Map map = getMap(actual); + + Assertions.assertThat(map).isNotNull(); + Assertions.assertThat(map.keySet()).containsAll(theKeys); + checkKeysAndValues(map, theKeys, theValues); + return this; + } + + private void checkKeysAndValues(Map theExpected, List theKeys, List 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 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 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 thePaths, List 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 getPathMap(String thePath) { + String[] pathElements = thePath.split("\\."); + StringBuilder pathSoFar = new StringBuilder(); + + Map 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 aMap = (Map) 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; + } + } +} diff --git a/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/AssertJsonTest.java b/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/AssertJsonTest.java new file mode 100644 index 00000000000..941450882fd --- /dev/null +++ b/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/AssertJsonTest.java @@ -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" + """); + } + + } + + +}