mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#2173 - ebean-test: Add assertContains - DbJson.of(entityBean).assertContains(...)
This commit is contained in:
@@ -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(".[", "[");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user