Java tutorial
/* * 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; } } }