All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.testhelpers.JsonAssertions Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
package com.launchdarkly.testhelpers;

import com.google.common.base.Joiner;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.launchdarkly.testhelpers.JsonTestValue.jsonFromValue;
import static com.launchdarkly.testhelpers.JsonTestValue.jsonOf;
import static org.hamcrest.MatcherAssert.assertThat;

/**
 * Test assertions and matchers related to JSON.
 * 

* The {@code assert} methods here provide simple assertions for strings that are assumed * to contain JSON. *

* The other methods are factories for type-safe Hamcrest matchers. These are much more * flexible, as you can use standard Hamcrest combinators like {@code allOf} or {@code not}. * These use {@link JsonTestValue} as their type parameter, to prevent confusion between * test code that operates on JSON strings and test code that operates on other kinds of * strings. {@link JsonTestValue} is easily convertible from strings or other types; * see {@link JsonTestValue#jsonOf(String)} and {@link JsonTestValue#jsonFromValue(Object)}. *

* Examples: *


 *     // check for the exact JSON properties {"a": 1, "b": 2} in any order
 *     assertThat(jsonOf(myString), jsonEquals("{\"a\":1, \"b\": 2}");
 *     
 *     // check that a JSON object's property "p" is equal to a specific boolean value
 *     assertThat(jsonOf(myString), jsonProperty("p", someBooleanValue));
 *     
 *     // check that a JSON object's property "p" is either null or omitted
 *     assertThat(jsonOf(myString),
 *         jsonProperty("p", anyOf(jsonNull(), jsonUndefined())));
 *     
 *     // check that a JSON object's property "p" is an array containing a specific value
 *     assertThat(jsonOf(myString),
 *         jsonProperty("p", isJsonArray(hasItem(jsonEqualsValue(someValue)))));
 * 
*

* When comparing unequal JSON objects or arrays, these methods will do their best to * show you a localized difference such as a specific property, rather than only showing * the entire actual and expected values. * * @since 1.1.0 */ public abstract class JsonAssertions { /** * Parses two strings as JSON and compares them for deep equality. If they are unequal, * it tries to describe the difference as specifically as possible by recursing into * object properties or array elements. * * @param expected the expected JSON string * @param actual the actual JSON string * @throws AssertionError if the values are not deeply equal, or are not valid JSON */ public static void assertJsonEquals(String expected, String actual) { assertThat(jsonOf(actual), jsonEquals(jsonOf(expected))); } /** * Equivalent to {@link #assertJsonEquals(String, String)}, but as a typed matcher. * * @param expected the expected JSON value * @return a matcher */ public static Matcher jsonEquals(final JsonTestValue expected) { checkNotNull(expected, "expected"); return new TypeSafeDiagnosingMatcher() { @Override public void describeTo(Description description) { description.appendText("JSON is equal to: " + expected); } @Override protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { if (!actual.isDefined()) { if (!expected.isDefined()) { return true; } mismatchDescription.appendValue(actual); return false; } if (!expected.isDefined()) { mismatchDescription.appendValue(expected); return false; } if (actual.parsed.equals(expected.parsed)) { return true; } String diff = describeJsonDifference(expected.parsed, actual.parsed, "", false); if (diff == null) { diff = "expected: " + expected + "\nactual: " + actual.raw; } else { diff = diff + "\nfull JSON was: " + actual.raw; } mismatchDescription.appendText(diff); return false; } }; } /** * Equivalent to {@code jsonEquals(JsonTestValue.jsonOf(expected))}. * * @param expected the expected JSON as a string * @return a matcher */ public static Matcher jsonEquals(String expected) { return jsonEquals(jsonOf(expected)); } /** * Equivalent to {@code jsonEquals(JsonTestValue.jsonFromValue(expected))}. * * @param expected a value that will be serialized to JSON and matched * @return a matcher */ public static Matcher jsonEqualsValue(Object expected) { return jsonEquals(jsonFromValue(expected)); } /** * Same as {@link #assertJsonEquals(String, String)} except that it allows any JSON * objects in the actual data to contain extra properties that are not in the expected * data. * * @param expected the expected JSON string * @param actual the actual JSON string * @throws AssertionError if the expected values are not a subset of the actual * values, or if the strings are not valid JSON */ public static void assertJsonIncludes(String expected, String actual) { assertThat(jsonOf(actual), jsonIncludes(expected)); } /** * Equivalent to {@link #assertJsonIncludes(String, String)}, but as a Hamcrest matcher. * * @param expected the expected JSON object properties * @return a string matcher */ public static Matcher jsonIncludes(JsonTestValue expected) { checkNotNull(expected, "expected"); return new TypeSafeDiagnosingMatcher() { @Override public void describeTo(Description description) { description.appendText("includes these JSON properties: " + expected); } @Override protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { if (!actual.isDefined()) { if (!expected.isDefined()) { return true; } mismatchDescription.appendValue(actual); return false; } if (!expected.isDefined()) { mismatchDescription.appendValue(expected); return false; } if (isJsonSubset(expected.parsed, actual.parsed)) { return true; } String diff = describeJsonDifference(expected.parsed, actual.parsed, "", true); if (diff == null) { diff = "expected: " + expected + "\nactual: " + actual.raw; } else { diff = diff + "\nfull JSON was: " + actual.raw; } mismatchDescription.appendText(diff); return false; } }; } /** * Equivalent to {@code jsonIncludes(JsonTestValue.jsonOf(expected))}. * * @param expected the expected JSON as a string * @return a matcher */ public static Matcher jsonIncludes(String expected) { return jsonIncludes(jsonOf(expected)); } /** * A matcher that verifies that the input value is a JSON null. This is equivalent to * {@code jsonEquals(JsonTestValue.jsonOf("null"))}. * * @return a matcher */ public static Matcher jsonNull() { return jsonEquals(jsonOf("null")); } /** * A matcher that verifies that the input value is completely undefined (as opposed to * being a JSON null). * * @return a matcher */ public static Matcher jsonUndefined() { return new TypeSafeDiagnosingMatcher() { @Override public void describeTo(Description description) { description.appendText("is undefined"); } @Override protected boolean matchesSafely(JsonTestValue item, Description mismatchDescription) { if (item == null || !item.isDefined()) { return true; } mismatchDescription.appendText("had value: " + item.raw); return false; } }; } /** * A matcher that verifies that the input value is an object that has a property with * the specified name, and that the property value matches the specified matcher. * * @param name the property name * @param matcher a matcher for the property value * @return a matcher */ public static Matcher jsonProperty(final String name, final Matcher matcher) { checkNotNull(name, "name"); checkNotNull(matcher, "matcher"); return new TypeSafeDiagnosingMatcher() { @Override public void describeTo(Description description) { description.appendText(String.format("property \"%s\": ", name)); matcher.describeTo(description); } @Override protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { if (!actual.isDefined()) { mismatchDescription.appendValue(actual); return false; } if (actual.parsed instanceof JsonObject) { JsonTestValue propValue = JsonTestValue.ofParsed(((JsonObject)actual.parsed).get(name)); if (!matcher.matches(propValue)) { matcher.describeMismatch(propValue, mismatchDescription); return false; } return true; } mismatchDescription.appendText("not a JSON object: ").appendText(actual.raw); return false; } }; } /** * A shortcut for using {@link #jsonProperty} with {@link #jsonEquals(JsonTestValue)}. * * @param name a property name * @param value the desired value * @return a matcher */ public static Matcher jsonProperty(String name, JsonTestValue value) { return jsonProperty(name, jsonEquals(value)); } /** * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with * {@link JsonTestValue#jsonFromValue(Object)}. * * @param name a property name * @param value a value that will be converted to JSON * @return a matcher */ public static Matcher jsonProperty(String name, boolean value) { return jsonProperty(name, jsonFromValue(value)); } /** * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with * {@link JsonTestValue#jsonFromValue(Object)}. * * @param name a property name * @param value a value that will be converted to JSON * @return a matcher */ public static Matcher jsonProperty(String name, int value) { return jsonProperty(name, jsonFromValue(value)); } /** * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with * {@link JsonTestValue#jsonFromValue(Object)}. * * @param name a property name * @param value a value that will be converted to JSON * @return a matcher */ public static Matcher jsonProperty(String name, double value) { return jsonProperty(name, jsonFromValue(value)); } /** * A shortcut for using {@link #jsonProperty(String, JsonTestValue)} with * {@link JsonTestValue#jsonFromValue(Object)}. * * @param name a property name * @param value a value that will be converted to JSON * @return a matcher */ public static Matcher jsonProperty(String name, String value) { return jsonProperty(name, jsonFromValue(value)); } /** * A matcher that verifies that the input value is an array whose elements match the * specified matchers. * * @param elementsMatcher a matcher for the contents of the array * @return a matcher */ public static Matcher isJsonArray(final Matcher> elementsMatcher) { checkNotNull(elementsMatcher, "elementsMatcher"); return new TypeSafeDiagnosingMatcher() { @Override public void describeTo(Description description) { description.appendText("JSON array: "); elementsMatcher.describeTo(description); } @Override protected boolean matchesSafely(JsonTestValue actual, Description mismatchDescription) { if (!actual.isDefined()) { mismatchDescription.appendValue(actual); return false; } if (actual.parsed instanceof JsonArray) { List values = new ArrayList<>(); for (JsonElement element: (JsonArray)actual.parsed) { values.add(JsonTestValue.ofParsed(element)); } if (!elementsMatcher.matches(values)) { elementsMatcher.describeMismatch(values, mismatchDescription); return false; } return true; } mismatchDescription.appendText("not a JSON array: ").appendText(actual.raw); return false; } }; } private static boolean isJsonSubset(JsonElement expected, JsonElement actual) { if (expected instanceof JsonObject && actual instanceof JsonObject) { JsonObject eo = (JsonObject)expected, ao = (JsonObject)actual; for (Map.Entry e: eo.entrySet()) { if (!ao.has(e.getKey()) || !isJsonSubset(e.getValue(), ao.get(e.getKey()))) { return false; } } return true; } if (expected instanceof JsonArray && actual instanceof JsonArray) { JsonArray ea = (JsonArray)expected, aa = (JsonArray)actual; if (ea.size() != aa.size()) { return false; } for (int i = 0; i < ea.size(); i++) { if (!isJsonSubset(ea.get(i), aa.get(i))) { return false; } } return true; } return actual.equals(expected); } private static String describeJsonDifference( JsonElement expected, JsonElement actual, String prefix, boolean allowExtraProps ) { if (actual instanceof JsonObject && expected instanceof JsonObject) { return describeJsonObjectDifference((JsonObject)expected, (JsonObject)actual, prefix, allowExtraProps); } if (actual instanceof JsonArray && expected instanceof JsonArray) { return describeJsonArrayDifference((JsonArray)expected, (JsonArray)actual, prefix, allowExtraProps); } return null; } private static String describeJsonObjectDifference( JsonObject expected, JsonObject actual, String prefix, boolean allowExtraProps ) { List diffs = new ArrayList<>(); Set allKeys = new HashSet<>(); for (Map.Entry e: expected.entrySet()) { allKeys.add(e.getKey()); } for (Map.Entry e: actual.entrySet()) { allKeys.add(e.getKey()); } for (String key: allKeys) { String prefixedKey = prefix + (prefix == "" ? "" : ".") + key; String expectedDesc = null, actualDesc = null, detailDiff = null; if (expected.has(key)) { if (actual.has(key)) { JsonElement actualValue = actual.get(key), expectedValue = expected.get(key); if (!actualValue.equals(expectedValue)) { expectedDesc = expectedValue.toString(); actualDesc = actualValue.toString(); detailDiff = describeJsonDifference(expectedValue, actualValue, prefixedKey, allowExtraProps); } } else { expectedDesc = expected.get(key).toString(); actualDesc = ""; } } else if (!allowExtraProps) { actualDesc = actual.get(key).toString(); expectedDesc = ""; } if (expectedDesc != null || actualDesc != null) { if (detailDiff != null) { diffs.add(detailDiff); } else { diffs.add(String.format("at \"%s\": expected = %s, actual = %s", prefixedKey, expectedDesc, actualDesc)); } } } return Joiner.on("\n").join(diffs); } private static String describeJsonArrayDifference( JsonArray expected, JsonArray actual, String prefix, boolean allowExtraProps ) { if (expected.size() != actual.size()) { return null; // can't provide a detailed diff, just show the whole values } List diffs = new ArrayList<>(); for (int i = 0; i < expected.size(); i++) { String prefixedIndex = String.format("%s[%d]", prefix, i); JsonElement actualValue = actual.get(i), expectedValue = expected.get(i); if (!actualValue.equals(expectedValue)) { String detailDiff = describeJsonDifference(expectedValue, actualValue, prefixedIndex, allowExtraProps); if (detailDiff != null) { diffs.add(detailDiff); } else { diffs.add(String.format("at \"%s\": expected = %s, actual = %s", prefixedIndex, expectedValue.toString(), actualValue.toString())); } } } return Joiner.on("\n").join(diffs); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy