org.unitils.mock.report.impl.ObservedInvocationsReport.java Source code

Java tutorial

Introduction

Here is the source code for org.unitils.mock.report.impl.ObservedInvocationsReport.java

Source

/*
 * Copyright 2013,  Unitils.org
 *
 * 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 org.unitils.mock.report.impl;

import org.unitils.core.util.ObjectFormatter;
import org.unitils.mock.core.ObservedInvocation;
import org.unitils.mock.core.proxy.Argument;
import org.unitils.mock.core.proxy.ProxyInvocation;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

import static org.apache.commons.lang.StringUtils.rightPad;
import static org.apache.commons.lang.StringUtils.uncapitalize;
import static org.unitils.core.util.ReflectionUtils.getAllFields;
import static org.unitils.core.util.ReflectionUtils.getFieldValue;

/**
 * A view that displays the observed invocations and the location where they were invoked.
 * The arguments are shown inline when the length is small enough, else the argument is named using the
 * type (eg Person => person1).
 * <p/>
 * Example: <pre><code>
 * 1.  mock.method1()) -> string1  ..... at MyTest.testMethod(MyTest.java:60)
 * 2.  mock.method1("bla", 4) -> null  ..... at MyTest.testMethod(MyTest.java:62)
 * 3.  mock.anotherMethod(myClass1)  ..... at MyTest.testMethod(MyTest.java:64)
 * <code></pre>
 *
 * @author Tim Ducheyne
 * @author Kenny Claes
 * @author Filip Neven
 */
public class ObservedInvocationsReport {

    protected int maxInlineParameterLength;
    protected ObjectFormatter objectFormatter;

    public ObservedInvocationsReport(ObjectFormatter objectFormatter, int maxInlineParameterLength) {
        this.objectFormatter = objectFormatter;
        this.maxInlineParameterLength = maxInlineParameterLength;
    }

    /**
     * Creates a string representation of the given invocations as described in the class javadoc.
     *
     * @param observedInvocations The invocations for which to create a report, not null
     * @param testObject          The test instance
     * @return The string representation, not null
     */
    public String createReport(List<ObservedInvocation> observedInvocations, Object testObject) {
        Map<Object, String> largeObjectNames = new IdentityHashMap<Object, String>();
        Map<Class<?>, Integer> largeObjectNameIndexes = new HashMap<Class<?>, Integer>();
        Map<Object, String> fieldValuesAndNames = getFieldValuesAndNames(testObject);

        return formatObservedInvocations(observedInvocations, largeObjectNames, largeObjectNameIndexes,
                fieldValuesAndNames);
    }

    protected String formatObservedInvocations(List<ObservedInvocation> observedInvocations,
            Map<Object, String> largeObjectNames, Map<Class<?>, Integer> largeObjectNameIndexes,
            Map<Object, String> fieldValuesAndNames) {
        StringBuilder result = new StringBuilder();
        int invocationIndex = 0;
        for (ObservedInvocation observedInvocation : observedInvocations) {
            List<FormattedObject> currentLargeObjects = new ArrayList<FormattedObject>();
            result.append(formatInvocationIndex(++invocationIndex, observedInvocations.size()));
            result.append(formatObservedInvocation(observedInvocation, currentLargeObjects, largeObjectNames,
                    largeObjectNameIndexes, fieldValuesAndNames));
            result.append(formatInvokedAt(observedInvocation));
        }
        return result.toString();
    }

    /**
     * Creates a string representation of the given invocation.
     * If arguments and result values are small enough, they are displayed inline, else the value is replaced by
     * a name generated by the {@link #createLargeValueName} method.
     *
     * @param observedInvocation     The invocation to format, not null
     * @param currentLargeObjects    The current the large values, not null
     * @param largeObjectNames       All large values names per value, not null
     * @param largeObjectNameIndexes The current indexes to use for the large value names (per value type), not null
     * @param fieldValuesAndNames    The values and name of the instance fields in the test object
     * @return The string representation, not null
     */
    protected String formatObservedInvocation(ObservedInvocation observedInvocation,
            List<FormattedObject> currentLargeObjects, Map<Object, String> largeObjectNames,
            Map<Class<?>, Integer> largeObjectNameIndexes, Map<Object, String> fieldValuesAndNames) {
        StringBuilder result = new StringBuilder();
        Method method = observedInvocation.getMethod();

        // append the mock and method name
        result.append(observedInvocation.getProxyName());
        result.append('.');
        result.append(method.getName());

        // append the arguments
        result.append('(');
        List<Argument<?>> arguments = observedInvocation.getArguments();
        if (!arguments.isEmpty()) {
            for (Argument<?> argument : arguments) {
                Class<?> argumentType = argument.getType();
                Object argumentValue = argument.getValue();
                Object argumentValueAtInvocationTime = argument.getValueAtInvocationTime();

                result.append(formatValue(argumentValueAtInvocationTime, argumentValue, argumentType,
                        currentLargeObjects, largeObjectNames, largeObjectNameIndexes, fieldValuesAndNames));
                result.append(", ");
            }
            // remove the last comma
            result.setLength(result.length() - 2);
        }
        result.append(")");

        // append the result value, if the method is non-void
        Class<?> resultType = method.getReturnType();
        if (Void.TYPE != resultType) {
            result.append(" -> ");
            Object resultValue = observedInvocation.getResult();
            Object resultAtInvocationTime = observedInvocation.getResultAtInvocationTime();
            result.append(formatValue(resultAtInvocationTime, resultValue, resultType, currentLargeObjects,
                    largeObjectNames, largeObjectNameIndexes, fieldValuesAndNames));
        }
        return result.toString();
    }

    /**
     * Formats the given value. If the value is small enough (length <=20), the value itself is returned, else
     * a name is returned generated by the {@link #createLargeValueName} method.
     * <p/>
     * E.g. string1, myClass1
     *
     * @param valueAtInvocationTime  The value to format, not null
     * @param value                  The value to format by reference, not null
     * @param type                   The type of the large value, not null
     * @param currentLargeObjects    The current the large values, not null
     * @param largeObjectNames       All large values names per value, not null
     * @param largeObjectNameIndexes The current indexes to use for the large value names (per value type), not null
     * @param fieldValuesAndNames    The values and name of the instance fields in the test object
     * @return The value or the replaced name, not null
     */
    protected String formatValue(Object valueAtInvocationTime, Object value, Class<?> type,
            List<FormattedObject> currentLargeObjects, Map<Object, String> largeObjectNames,
            Map<Class<?>, Integer> largeObjectNameIndexes, Map<Object, String> fieldValuesAndNames) {
        String valueName = largeObjectNames.get(valueAtInvocationTime);
        if (valueName == null) {
            valueName = largeObjectNames.get(value);
        }
        if (valueName == null) {
            valueName = fieldValuesAndNames.get(value);
        }
        String objectRepresentation = formatObject(valueAtInvocationTime);
        if (valueName == null) {
            if (objectRepresentation.length() <= maxInlineParameterLength) {
                // The object representation is small enough to be shown inline
                return objectRepresentation;
            }
            valueName = createLargeValueName(type, largeObjectNameIndexes);
        }
        FormattedObject formattedObject = new FormattedObject(valueName, objectRepresentation);
        largeObjectNames.put(valueAtInvocationTime, valueName);
        largeObjectNames.put(value, valueName);
        currentLargeObjects.add(formattedObject);
        return valueName;
    }

    /**
     * Creates a string representation of the details of the given invocation. This will give information about
     * where the invocation occurred.
     *
     * @param proxyInvocation The invocation to format, not null
     * @return The string representation, not null
     */
    protected String formatInvokedAt(ProxyInvocation proxyInvocation) {
        return "  .....  at " + proxyInvocation.getInvokedAt() + "\n";
    }

    /**
     * Creates a name to replace a large value.
     * The name is derived from the given type and index. E.g. string1, myClass1
     *
     * @param type                   The type of the large value, not null
     * @param largeObjectNameIndexes The current indexes per type, not null
     * @return The name, not null
     */
    protected String createLargeValueName(Class<?> type, Map<Class<?>, Integer> largeObjectNameIndexes) {
        Integer index = largeObjectNameIndexes.get(type);
        if (index == null) {
            index = 0;
        }
        largeObjectNameIndexes.put(type, ++index);

        String result = uncapitalize(type.getSimpleName());
        return result + index;
    }

    /**
     * Formats the invocation number, and adds spaces to make sure everything is formatted
     * nicely on the same line width.
     *
     * @param invocationIndex       The index of the invocation
     * @param totalInvocationNumber The total number of invocations.
     * @return The formatted invocation number
     */
    protected String formatInvocationIndex(int invocationIndex, int totalInvocationNumber) {
        int padSize = String.valueOf(totalInvocationNumber).length() + 2;
        return rightPad(invocationIndex + ".", padSize);
    }

    /**
     * @param object The object
     * @return A string representation of the object, not null
     */
    protected String formatObject(Object object) {
        return objectFormatter.format(object);
    }

    /**
     * Gets all the field values in the given test object with their corresponding field names.
     *
     * @param testedObject The test object
     * @return The values and names in an identity map, empty if tested object is null
     */
    protected Map<Object, String> getFieldValuesAndNames(Object testedObject) {
        Map<Object, String> result = new IdentityHashMap<Object, String>();
        if (testedObject == null) {
            return result;
        }
        Set<Field> fields = getAllFields(testedObject.getClass());
        for (Field field : fields) {
            Object value = getFieldValue(testedObject, field);
            if (value != null) {
                result.put(value, field.getName());
            }
        }
        return result;
    }

    /**
     * Class for representing a value that was too large to be displayed inline.
     */
    protected static class FormattedObject {

        /* The name used as inline replacement */
        protected String name;
        /* The actual string representation of the value */
        protected String representation;

        /**
         * Creates a large value
         *
         * @param name           The name used as inline replacement, not null
         * @param representation The actual string representation of the value, not null
         */
        public FormattedObject(String name, String representation) {
            this.name = name;
            this.representation = representation;
        }

        /**
         * @return The name used as inline replacement, not null
         */
        public String getName() {
            return name;
        }

        /**
         * @return The actual string representation of the value, not null
         */
        public String getRepresentation() {
            return representation;
        }
    }
}