Blog

Tuesday, 16 October 2012

Mocking FindBugs

In previous post on writing FindBugs plugin I've said it is very immune to testing. Since today it's different.

It is full of final classes that can't be mocked with Mockito.
Well I've managed to work some things out and now our itcrowd-domain-findbugs gained some unit tests.
Take a look at https://github.com/it-crowd/itcrowd-domain-findbugs/blob/master/impl/src/test/java/pl/com/it_crowd/findbugs/BcelMockHelper.java.

package pl.com.it_crowd.findbugs;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.AnnotationElementValue;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.Field;

import java.util.ArrayList;
import java.util.StringTokenizer;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public final class BcelMockHelper {
// -------------------------- STATIC METHODS --------------------------

    public static AnnotationEntry mockAnnotationEntry(String annotationType, String... properties)
    {
        return mockAnnotationEntry(annotationType, true, properties);
    }

    public static AnnotationEntry mockAnnotationEntry(String annotationType, boolean runtimeVisible, String... properties)
    {
        final AnnotationEntry entry = mock(AnnotationEntry.class);
        when(entry.getAnnotationType()).thenReturn(annotationType);
        when(entry.isRuntimeVisible()).thenReturn(runtimeVisible);
        final ArrayList<ElementValuePair> elementValuePairs = new ArrayList<ElementValuePair>();
        for (String property : properties) {
            final StringTokenizer tokenizer = new StringTokenizer(property, "=");
            final String key = tokenizer.nextToken();
            final String value = tokenizer.nextToken();
            final AnnotationElementValue elementValue = mock(AnnotationElementValue.class);
            when(elementValue.stringifyValue()).thenReturn(value);
            final ElementValuePair pair = new ElementValuePair(0, elementValue, mockConstantPool(0, Constants.CONSTANT_Utf8, new ConstantUtf8(key)));
            elementValuePairs.add(pair);
        }
        when(entry.getElementValuePairs()).thenReturn(elementValuePairs.toArray(new ElementValuePair[elementValuePairs.size()]));
        return entry;
    }

    public static ConstantPool mockConstantPool(int index, byte tag, final Constant value)
    {
        final ConstantPool pool = mock(ConstantPool.class);
        when(pool.getConstant(index, tag)).thenReturn(value);
        return pool;
    }

    public static Field mockField(String type, AnnotationEntry... annotationEntries)
    {
        final org.apache.bcel.classfile.Annotations annotations = mock(org.apache.bcel.classfile.Annotations.class);
        when(annotations.getAnnotationEntries()).thenReturn(annotationEntries);
        return new Field(0, 0, 1, new Attribute[]{annotations}, mockConstantPool(1, Constants.CONSTANT_Utf8, new ConstantUtf8(type)));
    }

// --------------------------- CONSTRUCTORS ---------------------------

    private BcelMockHelper()
    {
    }
}

The problematic classes are ElementValuePair, Field and Method. ElementValuePair has getValue method which we use but it is final, so cannot be mocked with Mockito. Field and Method classes are both final so also immune to Mockito.
Also our detectors that extend EntityAnnotationDetector and thus AnnotationVisitor are problematic because AnnotationVisitor has static DEBUG attribute that is initialized when class is loaded, and that initializing code causes trouble.

Well we could live without testing detectors if we would be able to delegate most of their logic to some other class.
Ok so we still have those ElementValuePair, Field and Method. It turns out that we don't need to mock those classes entirely. We can create new instances with appropriate parameters. Look at line 39. ElementValuePair's constructor needs 3 params:
  1. index of it's name in ConstantPool
  2. instance of ElementValue which represents it's value
  3. ConstantPool
ConstantPool is holder of "constants" and classes usually use it to get values from it with getConstant(int,byte) method. You need to see the source code of method you want to call in your test how ConstantPool is used.
Ok, so back to line 39. We tell the constructor that name is under index 0 in the constant pool. The ElementValue and ConstantPool are mocked with Mockito. Note that the index you pass to mockConstantPool method is also 0. It's not coincidence, those indexes must be the same.

Now lets moveto mockField method. In itcrowd-domain-findbugs I'm mostly iterating over fields and methods and check the annotations so for unit tests I need to create fields with annotations.
That's what mockField method is for. It accepts string representation of field's type and any number of AnnotationEntry instances. AnnotationEntry instances can be mocked with mockAnnotationEntry method which accepts string representation of it's type,  boolean telling whether annotation is visible at runtime or not an any number of strings which should be in form of "key=value" where key is annotation's attribute and value is... the value of annotation's attribute.
So with i.e.  mockAnnotationEntry("Ljavax/persistence/Column;","name=ID","nullable=false") we simulate @javax.persistence.Column(name="ID", nullable=false).

You may wonder what that "Ljavax/persistence/Column;" is. Well this is what would ((Type)type).getSignature() return.
Here is a list of signatures for several types that will give you the idea:
  • I for int
  • Ljava/lang/Integer; for Integer
  • [I for int[]
 Another class you might want to look at is https://github.com/it-crowd/itcrowd-domain-findbugs/blob/master/impl/src/main/java/pl/com/it_crowd/findbugs/BcelHelper.java
package pl.com.it_crowd.findbugs;

import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ArrayType;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.Type;

import java.util.Collection;
import java.util.Map;

public final class BcelHelper {
// ------------------------------ FIELDS ------------------------------

    private static final ObjectType COLLECTION_TYPE;

    private static final ObjectType MAP_TYPE;

// -------------------------- STATIC METHODS --------------------------

    static {
        COLLECTION_TYPE = (ObjectType) Type.getType(Collection.class);
        MAP_TYPE = (ObjectType) Type.getType(Map.class);
    }

    public static String getAnnotationPropertyValue(AnnotationEntry entry, String propertyName)
    {
        if (propertyName == null) {
            return null;
        }
        for (ElementValuePair pair : entry.getElementValuePairs()) {
            if (propertyName.equals(pair.getNameString())) {
                return pair.getValue().stringifyValue();
            }
        }
        return null;
    }

    public static String getAnnotationPropertyValue(AnnotationEntry entry, String propertyName, Object defaultValue)
    {
        final String value = getAnnotationPropertyValue(entry, propertyName);
        return value == null ? (defaultValue == null ? null : defaultValue.toString()) : value;
    }

    public static Type getType(FieldOrMethod obj)
    {
        if (obj instanceof Field) {
            return ((Field) obj).getType();
        } else {
            return ((Method) obj).getReturnType();
        }
    }

    public static boolean isArray(Type type)
    {
        return type instanceof ArrayType;
    }

    public static boolean isCollection(Type type)
    {
        try {
            return type instanceof ObjectType && ((ObjectType) type).isCastableTo(COLLECTION_TYPE);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean isJavaxPersistenceColumn(AnnotationEntry entry)
    {
        return entry != null && Annotations.JAVAX_PERSISTENCE_COLUMN.equals(entry.getAnnotationType());
    }

    public static boolean isJavaxPersistenceColumnOrJoinColumn(AnnotationEntry entry)
    {
        return isJavaxPersistenceColumn(entry) || isJavaxPersistenceJoinColumn(entry);
    }

    public static boolean isJavaxPersistenceJoinColumn(AnnotationEntry entry)
    {
        return entry != null && Annotations.JAVAX_PERSISTENCE_JOIN_COLUMN.equals(entry.getAnnotationType());
    }

    public static boolean isMap(Type type)
    {
        try {
            return type instanceof ObjectType && ((ObjectType) type).isCastableTo(MAP_TYPE);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean isString(Type type)
    {
        return type instanceof ObjectType && String.class.getCanonicalName().equals(((ObjectType) type).getClassName());
    }

// --------------------------- CONSTRUCTORS ---------------------------

    private BcelHelper()
    {
    }
}

I've highlighted lines that might interest you and are self explanatory. We don't have to work with strings from ((Type)type).getSignature() or ((Type)type).toString() to determine type.

Learning this things helped me a lot to get the code testable with unit tests. I hope it helps you too. I'm waiting for your precious feedback.

No comments:

Post a Comment