com.scvngr.levelup.core.test.JsonTestUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.scvngr.levelup.core.test.JsonTestUtil.java

Source

/*
 * Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.scvngr.levelup.core.test;

import android.support.annotation.NonNull;
import android.test.MoreAsserts;

import com.scvngr.levelup.core.annotation.JsonValueType;
import com.scvngr.levelup.core.annotation.JsonValueType.JsonType;
import com.scvngr.levelup.core.model.factory.json.AbstractJsonModelFactory;
import com.scvngr.levelup.core.util.LogManager;
import com.scvngr.levelup.core.util.NullUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Utility methods for testing.
 */
public final class JsonTestUtil {
    /**
     * <p>
     * Checks that modifying individual fields in a model will result in its equals/hashCode methods
     * failing. Uses reflection on {@link JsonValueType} annotations on fields of a passed class to
     * figure out how to modify the JSON representation of the model in different ways, then parses
     * the JSON with a {@link AbstractJsonModelFactory} subclass before checking equals/hashcode on
     * both the original and a modified object.
     * </p>
     * <p>
     * This effectively checks that equals/hashcode works across any value changes from fields we
     * read from JSON, but also checks some other potential issues. We're implicitly checking that
     * the JSON typing declared in annotations for the fields matches what we actually use when
     * parsing our JSON (since if it doesn't, we'll get JSON errors when reading the data during the
     * clone/modify). We're also checking for fields that may have been added to the JSON keys and
     * the model without updating equals/hashcode to reflect them (as long as they're declared in
     * the JSONKeys class used here).
     * </p>
     * <p>
     * Note that this is only intended for test use and will turn all checked exceptions it might
     * throw into unchecked ones.
     * </p>
     *
     * @param jsonKeysClass Class of the underlying keys class to test all fields (except
     *        blacklistFields) from. Must have visible fields to read from.
     * @param jsonFactory Factory object to construct model instances from out of the base and
     *        generated-variant JSON objects before checking equals/hashcode.
     * @param baseJsonObject Fully-populated JSON object for the model to use for comparison with
     *        modified copies.
     * @param blacklistFields Fields to exclude from variant testing (either because we need to test
     *        them manually or because they don't reflect fields that are used for parsing into the
     *        model). Note that this is the jsonKeysClass's field name as a string, not the JSON key
     *        value (eg "ID", not "id").
     */
    public static void checkEqualsAndHashCodeOnJsonVariants(@NonNull final Class<?> jsonKeysClass,
            @NonNull final AbstractJsonModelFactory<?> jsonFactory, @NonNull final JSONObject baseJsonObject,
            @NonNull final String[] blacklistFields) {
        Object originalModel;
        Object differentModel;
        Object differentModelReparse;

        try {
            originalModel = jsonFactory.from(baseJsonObject);
        } catch (final JSONException e1) {
            throw new RuntimeException(e1);
        }

        MoreAsserts.checkEqualsAndHashCodeMethods(originalModel, null, false);

        final Field[] jsonKeyFields = jsonKeysClass.getFields();
        final List<String> blacklisted = Arrays.asList(blacklistFields);
        final String key = null;

        MoreAsserts.assertNotEmpty("JSON keys class visible fields", Arrays.asList(jsonKeyFields));

        for (final Field field : jsonKeyFields) {
            if (!blacklisted.contains(field.getName())) {
                JSONObject copiedDifferingObject;
                String fieldString;
                // Don't check exceptions, just let tests fail.
                try {
                    fieldString = NullUtils.nonNullContract((String) field.get(key));
                    copiedDifferingObject = cloneObjectDifferingOnParam(baseJsonObject, fieldString,
                            reflectJsonType(field));
                    differentModel = jsonFactory.from(copiedDifferingObject);
                    differentModelReparse = jsonFactory.from(copiedDifferingObject);
                } catch (final IllegalArgumentException e) {
                    throw new RuntimeException(e);
                } catch (final IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (final JSONException e) {
                    throw new RuntimeException(e);
                }

                MoreAsserts.checkEqualsAndHashCodeMethods(
                        String.format(Locale.US, "Modified %s and checked equals and hash", fieldString),
                        originalModel, differentModel, false);
                MoreAsserts.checkEqualsAndHashCodeMethods(
                        String.format(Locale.US, "Modified %s and checked equals and hash", fieldString),
                        differentModel, differentModel, true);
                MoreAsserts.checkEqualsAndHashCodeMethods(
                        String.format(Locale.US, "Modified %s and checked equals and hash", fieldString),
                        differentModel, differentModelReparse, true);
            }
        }
    }

    /**
     * Reflects a {@link JsonType} from a field's {@link JsonValueType} annotation.
     *
     * @param field Field with a JsonValueType annotation to read from.
     * @return the {@link JsonType} of the field.
     */
    @NonNull
    private static JsonType reflectJsonType(@NonNull final Field field) {
        final JsonValueType annotation = field.getAnnotation(JsonValueType.class);
        return annotation.value();
    }

    /**
     * Makes a deep copy of a JSONObject, modifying one key based on its {@link JsonType} (e.g.
     * flipping the value for a boolean, adding 1 to numbers, appending data to a string).
     *
     * @param baseObject JSONObject to create a modified deep copy of.
     * @param key Key to modify the value of in the copy.
     * @param jsonType {@link JsonType} to use to infer how to modify the copy's value.
     * @return a deep copy of the JSON object, with one modified field.
     * @throws JSONException if there was a parsing error.
     */
    @NonNull
    private static JSONObject cloneObjectDifferingOnParam(@NonNull final JSONObject baseObject,
            @NonNull final String key, @NonNull final JsonType jsonType) throws JSONException {
        final JSONObject object = new JSONObject(baseObject.toString());
        LogManager.d("Testing field %s", key);

        if (JsonType.BOOLEAN.equals(jsonType)) {
            object.put(key, !object.getBoolean(key));
        } else if (JsonType.DOUBLE.equals(jsonType)) {
            object.put(key, object.getDouble(key) + 1);
        } else if (JsonType.INT.equals(jsonType)) {
            object.put(key, object.getInt(key) + 1);
        } else if (JsonType.LONG.equals(jsonType)) {
            object.put(key, object.getLong(key) + 1);
        } else if (JsonType.STRING.equals(jsonType)) {
            String currentString = object.getString(key);

            if (null == currentString) {
                currentString = "";
            }

            object.put(key, currentString + "_testdifferent");

        } else if (JsonType.JSON_ARRAY.equals(jsonType)) {
            JSONArray currentArray = object.getJSONArray(key);

            if (null == currentArray) {
                currentArray = new JSONArray();
            }

            final JSONArray modifiedArray = new JSONArray(currentArray.toString());
            modifiedArray.put(1);
            object.put(key, modifiedArray);
        } else {
            throw new UnsupportedOperationException(String.format(Locale.US,
                    "Can only use for JsonTypes(int/long/bool/string/array) not JsonType(%s)", jsonType.name()));
        }

        return object;
    }

    /**
     * Private constructor prevents instantiation.
     *
     * @throws UnsupportedOperationException because this class cannot be instantiated.
     */
    private JsonTestUtil() {
        throw new UnsupportedOperationException("This class is non-instantiable");
    }
}