#2173 - ebean-test: Add assertContains - DbJson.of(entityBean).assertContains(...)

This commit is contained in:
Robin Bygrave
2021-02-19 09:29:06 +13:00
parent b4ae721998
commit b7da2cbd97
23 changed files with 727 additions and 31 deletions
@@ -2,34 +2,63 @@ package io.ebean.test;
import io.ebean.DB;
import java.io.IOException;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Helper for testing to assert that the JSON form of an entity
* or list of entities match a String / typically test resource.
* or list of entities is as expected.
* <p>
* Using assertContains() we can match the JSON form of an entity
* against a subset of JSON content. Typically the subset of JSON
* excludes generated properties like id, when created and
* when modified properties.
* </p>
*
* <h3>Assert contains</h3>
* <pre>{@code
*
* DbJson.of(order)
* .assertContains("/order-partial.json");
*
* }</pre>
*
*
* <h3>Assert content matches</h3>
* <p>
* Using assertContentMatches() we are doing an exact content match.
* We typically need to replace generated property values. This assert
* will start to fail if the model changes like adding a property to
* the entity so we should use it less widely due to the maintenance
* burden we have with it.
* </p>
*
* <pre>{@code
*
* DbJson.of(timedEntries)
* .replace("id", "eventTime")
* .assertContentMatches("/assertJson/full-1-timed.json");
* DbJson.of(order)
* .replace("id", "whenCreated", "whenModified)
* .assertContentMatches("/order-full.json");
*
* }</pre>
*/
public class DbJson {
/**
* Create a PrettyJson object that has the JSON form of the
* entity bean or beans.
* Create a PrettyJson object that has the JSON form of the entity bean or beans.
*
* <h3>Assert contains</h3>
* <pre>{@code
*
* DbJson.of(timedEntries)
* .replace("id", "eventTime")
* .assertContentMatches("/assertJson/full-1-timed.json");
* DbJson.of(order)
* .assertContains("/order-partial.json");
*
* }</pre>
*
* <h3>Assert content matches</h3>
* <pre>{@code
*
* DbJson.of(order)
* .replace("id", "whenCreated", "whenModified)
* .assertContentMatches("/order-full.json");
*
* }</pre>
*/
@@ -41,12 +70,7 @@ public class DbJson {
* Read the content for the given resource path.
*/
public static String readResource(String resourcePath) {
InputStream is = DbJson.class.getResourceAsStream(resourcePath);
try {
return IOUtils.readUtf8(is).trim();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
return IOUtils.readResource(resourcePath);
}
/**
@@ -92,7 +116,7 @@ public class DbJson {
}
/**
* Assert the json matches the content at the given resource path.
* Assert the json exactly matches the content at the given resource path.
*
* <pre>{@code
*
@@ -103,7 +127,44 @@ public class DbJson {
* }</pre>
*/
public void assertContentMatches(String resourcePath) {
assertThat(rawJson).isEqualTo(readResource(resourcePath));
assertThat(lineEnd(rawJson)).isEqualTo(lineEnd(readResource(resourcePath)));
}
/**
* Normalise line ending characters to just use new line.
*/
private String lineEnd(String content) {
return content.replace("\r\n", "\n");
}
/**
* Assert the DB json contains the given json content.
* <p>
* With this "contains" check the DB Json can contain more content than what
* it is checked against. Typically the DB json can contain generated properties
* like id values, when created, when modified etc and we leave these out of the
* json content we are checking against.
* </p>
*
* @param json The subset json content that should be contained by the DB json.
*/
public void assertContains(String json) {
Json.assertContains(rawJson, json);
}
/**
* Assert the DB Json contains the Json at the given resource path.
* <p>
* With this "contains" check the DB Json can contain more content than what
* it is checked against. Typically the DB json can contain generated properties
* like id values, when created, when modified etc and we leave these out of the
* json content we are checking against.
* </p>
*
* @param resourcePath The resource path of the JSON content we are checking against.
*/
public void assertContainsResource(String resourcePath) {
assertContains(readResource(resourcePath));
}
}
}
@@ -11,6 +11,18 @@ import java.nio.charset.StandardCharsets;
*/
class IOUtils {
/**
* Read the content for the given resource path.
*/
static String readResource(String resourcePath) {
try {
InputStream is = IOUtils.class.getResourceAsStream(resourcePath);
return IOUtils.readUtf8(is).trim();
} catch (IOException e) {
throw new IllegalArgumentException("Error reading resource " + resourcePath, e);
}
}
/**
* Reads the entire contents of the specified input stream and return them as UTF-8 string.
*/
@@ -0,0 +1,168 @@
package io.ebean.test;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.util.List;
/**
* Helper to convert to and from json using Jackson object mapper and
* perform some useful asserts based on json content.
*/
public class Json {
/**
* For reading resource json content into a bean, list or jsonNode in a fluid style.
*/
public static class Resource {
private final String resourcePath;
private Resource(String resourcePath) {
this.resourcePath = resourcePath;
}
/**
* Return as a plain bean.
*/
<T> T asBean(Class<T> cls) {
return Json.read(cls, readResource(resourcePath));
}
/**
* Return as a list of beans.
*/
<T> List<T> asList(Class<T> cls) {
return Json.readList(cls, readResource(resourcePath));
}
/**
* Return as a JsonNode.
*/
JsonNode asNode() {
return Json.readNodeFromResource(resourcePath);
}
}
private static final ObjectMapper MAPPER = initMapper();
/**
* For fluid style reading resource json content and return as a
* bean, list of bean or JsonNode.
* <pre>{@code
*
* PlainBean bean =
* Json.resource("/example/plain-list.json").asBean(PlainBean.class);
*
* List<PlainBean> list =
* Json.resource("/example/plain-list.json").asList(PlainBean.class);
*
* JsonNode jsonNode =
* Json.resource("/example/plain-list.json").asJsonNode();
*
* }</pre>
*
* @param resourcePath The resource path where the json content is read from
* @return The resource to convert to a bean or jsonNode etc
*/
public static Resource resource(String resourcePath) {
return new Resource(resourcePath);
}
/**
* Assert all the fields in the expectedJson are present in actualJson and values match.
*/
public static void assertContains(String actualJson, String expectedJson) {
assertContains(readNode(actualJson), readNode(expectedJson));
}
/**
* Assert all the fields in the expectedJson are present in actualJson and values match.
*/
public static void assertContains(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
JsonAssertContains.assertContains(actualJsonNode, expectedJsonNode);
}
/**
* Read the content for the given resource path.
*/
public static String readResource(String resourcePath) {
return IOUtils.readResource(resourcePath);
}
/**
* Return a bean from json content of a resource path.
*/
public static <T> T readFromResource(Class<T> type, String resourcePath) {
return read(type, readResource(resourcePath));
}
/**
* Return a typed object from json content.
*/
public static <T> T read(Class<T> type, String json) {
try {
return MAPPER.readValue(json, type);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Return a list of a given type from json content.
*/
public static <T> List<T> readList(Class<T> type, String json) {
final CollectionType collectionType = MAPPER.getTypeFactory().constructCollectionType(List.class, type);
try {
return MAPPER.readValue(json, collectionType);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Parse json into a generic JsonNode structure from resource.
*/
public static JsonNode readNodeFromResource(String resourcePath) {
return readNode(IOUtils.readResource(resourcePath));
}
/**
* Parse json into a generic JsonNode structure.
*/
public static JsonNode readNode(String json) {
try {
return MAPPER.readTree(json);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Serialize object to string
*/
public static String toJsonString(Object bean) {
try {
return MAPPER.writeValueAsString(bean);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static ObjectMapper initMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.configure(SerializationFeature.INDENT_OUTPUT, true)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}
@@ -0,0 +1,118 @@
package io.ebean.test;
import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.api.Assertions;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Stack;
/**
* Perform traversal of JsonNodes comparing against an expected JsonNode that
* typically contains a subset of the data (typically excludes any generated properties
* like when modified timestamps etc).
*/
class JsonAssertContains {
private final Stack<String> path = new Stack<>();
private final LinkedList<String> errors = new LinkedList<>();
static void assertContains(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
new JsonAssertContains().contains(actualJsonNode, expectedJsonNode);
}
private void contains(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
checkRecursive(null, actualJsonNode, expectedJsonNode);
if (!errors.isEmpty()) {
String errorsString = String.join("\n", errors);
errorsString += "\nExpected JSON fields: " + expectedJsonNode;
errorsString += "\nActual JSON: " + actualJsonNode;
Assertions.fail(errorsString);
}
}
private void checkRecursive(String name, JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (name != null) {
path.push(name);
}
if (checkNull(actualJsonNode, expectedJsonNode)) {
if (checkType(actualJsonNode, expectedJsonNode)) {
if (checkArray(actualJsonNode, expectedJsonNode)) {
if (checkObject(actualJsonNode, expectedJsonNode)) {
checkValue(actualJsonNode, expectedJsonNode);
}
}
}
}
if (name != null) {
path.pop();
}
}
private boolean checkNull(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (actualJsonNode == null) {
errors.add(String.format("Expected field '%s' to be '%s' but was null", path(), expectedJsonNode));
return false;
}
return true;
}
private boolean checkType(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.getNodeType().equals(actualJsonNode.getNodeType())) {
errors.add(String.format("Expected field '%s' to be of type '%s' but was '%s'", path(), expectedJsonNode.getNodeType(), actualJsonNode.getNodeType()));
return false;
}
return true;
}
private boolean checkArray(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.isArray()) {
return true;
}
for (int i = 0; i < expectedJsonNode.size(); i++) {
checkRecursive("[" + i + "]", actualJsonNode.get(i), expectedJsonNode.get(i));
}
// do not continue (object or scalar type check)
return false;
}
private boolean checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.isObject()) {
return true;
}
Iterator<Map.Entry<String, JsonNode>> expectedFields = expectedJsonNode.fields();
while (expectedFields.hasNext()) {
Map.Entry<String, JsonNode> expectedField = expectedFields.next();
String expectedKey = expectedField.getKey();
JsonNode actualNode = actualJsonNode.get(expectedKey);
if (actualNode == null) {
errors.add(String.format("Expected field '%s' to be present", path(expectedKey)));
} else {
checkRecursive(expectedKey, actualNode, expectedField.getValue());
}
}
// do not continue (scalar type check)
return false;
}
private void checkValue(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.equals(actualJsonNode)) {
errors.add(String.format("Expected field '%s' to be equal to '%s' but was '%s'", path(), expectedJsonNode, actualJsonNode));
}
}
String path(String expectedKey) {
if (path.isEmpty()) {
return expectedKey;
}
return path() + "." + expectedKey;
}
String path() {
if (path.isEmpty()) {
return "";
}
return String.join(".", path).replace(".[", "[");
}
}