Home | History | Annotate | Download | only in mocking
      1 /*
      2  * Copyright 2010 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.google.android.testing.mocking;
     17 
     18 import javassist.CannotCompileException;
     19 import javassist.ClassClassPath;
     20 import javassist.ClassPool;
     21 import javassist.CtClass;
     22 import javassist.CtConstructor;
     23 import javassist.CtField;
     24 import javassist.CtMethod;
     25 import javassist.CtNewConstructor;
     26 import javassist.NotFoundException;
     27 
     28 import java.io.IOException;
     29 import java.lang.reflect.Constructor;
     30 import java.lang.reflect.Method;
     31 import java.lang.reflect.Modifier;
     32 import java.util.ArrayList;
     33 import java.util.Arrays;
     34 import java.util.HashMap;
     35 import java.util.List;
     36 import java.util.Map;
     37 
     38 
     39 /**
     40  * AndroidMockGenerator creates the subclass and interface required for mocking
     41  * a given Class.
     42  *
     43  * The only public method of AndroidMockGenerator is createMocksForClass. See
     44  * the javadocs for this method for more information about AndroidMockGenerator.
     45  *
     46  * @author swoodward (at) google.com (Stephen Woodward)
     47  */
     48 class AndroidMockGenerator {
     49   public AndroidMockGenerator() {
     50     ClassPool.doPruning = false;
     51     ClassPool.getDefault().insertClassPath(new ClassClassPath(MockObject.class));
     52   }
     53 
     54   /**
     55    * Creates a List of javassist.CtClass objects representing all of the
     56    * interfaces and subclasses required to meet the Mocking requests of the
     57    * Class specified by {@code clazz}.
     58    *
     59    * A test class can request that a Class be prepared for mocking by using the
     60    * {@link UsesMocks} annotation at either the Class or Method level. All
     61    * classes specified by these annotations will have exactly two CtClass
     62    * objects created, one for a generated interface, and one for a generated
     63    * subclass. The interface and subclass both define the same methods which
     64    * comprise all of the mockable methods of the provided class. At present, for
     65    * a method to be mockable, it must be non-final and non-static, although this
     66    * may expand in the future.
     67    *
     68    * The class itself must be mockable, otherwise this method will ignore the
     69    * requested mock and print a warning. At present, a class is mockable if it
     70    * is a non-final publicly-instantiable Java class that is assignable from the
     71    * java.lang.Object class. See the javadocs for
     72    * {@link java.lang.Class#isAssignableFrom(Class)} for more information about
     73    * what "is assignable from the Object class" means. As a non-exhaustive
     74    * example, if a given Class represents an Enum, Annotation, Primitive or
     75    * Array, then it is not assignable from Object. Interfaces are also ignored
     76    * since these need no modifications in order to be mocked.
     77    *
     78    * @param clazz the Class object to have all of its UsesMocks annotations
     79    *        processed and the corresponding Mock Classes created.
     80    * @return a List of CtClass objects representing the Classes and Interfaces
     81    *         required for mocking the classes requested by {@code clazz}
     82    * @throws ClassNotFoundException
     83    * @throws CannotCompileException
     84    * @throws IOException
     85    */
     86   public List<GeneratedClassFile> createMocksForClass(Class<?> clazz)
     87       throws ClassNotFoundException, IOException, CannotCompileException {
     88     return this.createMocksForClass(clazz, SdkVersion.UNKNOWN);
     89   }
     90 
     91   public List<GeneratedClassFile> createMocksForClass(Class<?> clazz, SdkVersion sdkVersion)
     92       throws ClassNotFoundException, IOException, CannotCompileException {
     93     if (!classIsSupportedType(clazz)) {
     94       reportReasonForUnsupportedType(clazz);
     95       return Arrays.asList(new GeneratedClassFile[0]);
     96     }
     97     CtClass newInterfaceCtClass = generateInterface(clazz, sdkVersion);
     98     GeneratedClassFile newInterface = new GeneratedClassFile(newInterfaceCtClass.getName(),
     99         newInterfaceCtClass.toBytecode());
    100     CtClass mockDelegateCtClass = generateSubClass(clazz, newInterfaceCtClass, sdkVersion);
    101     GeneratedClassFile mockDelegate = new GeneratedClassFile(mockDelegateCtClass.getName(),
    102         mockDelegateCtClass.toBytecode());
    103     return Arrays.asList(new GeneratedClassFile[] {newInterface, mockDelegate});
    104   }
    105 
    106   private void reportReasonForUnsupportedType(Class<?> clazz) {
    107     String reason = null;
    108     if (clazz.isInterface()) {
    109       // do nothing to make sure none of the other conditions apply.
    110     } else if (clazz.isEnum()) {
    111       reason = "Cannot mock an Enum";
    112     } else if (clazz.isAnnotation()) {
    113       reason = "Cannot mock an Annotation";
    114     } else if (clazz.isArray()) {
    115       reason = "Cannot mock an Array";
    116     } else if (Modifier.isFinal(clazz.getModifiers())) {
    117       reason = "Cannot mock a Final class";
    118     } else if (clazz.isPrimitive()) {
    119       reason = "Cannot mock primitives";
    120     } else if (!Object.class.isAssignableFrom(clazz)) {
    121       reason = "Cannot mock non-classes";
    122     } else if (!containsUsableConstructor(clazz)) {
    123       reason = "Cannot mock a class with no public constructors";
    124     } else {
    125       // Whatever the reason is, it's not one that we care about.
    126     }
    127     if (reason != null) {
    128       // Sometimes we want to be silent, so check 'reason' against null.
    129       System.err.println(reason + ": " + clazz.getName());
    130     }
    131   }
    132 
    133   private boolean containsUsableConstructor(Class<?> clazz) {
    134     Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    135     for (Constructor<?> constructor : constructors) {
    136       if (Modifier.isPublic(constructor.getModifiers()) ||
    137           Modifier.isProtected(constructor.getModifiers())) {
    138         return true;
    139       }
    140     }
    141     return false;
    142   }
    143 
    144   boolean classIsSupportedType(Class<?> clazz) {
    145     return (containsUsableConstructor(clazz)) && Object.class.isAssignableFrom(clazz)
    146         && !clazz.isInterface() && !clazz.isEnum() && !clazz.isAnnotation() && !clazz.isArray()
    147         && !Modifier.isFinal(clazz.getModifiers());
    148   }
    149 
    150   void saveCtClass(CtClass clazz) throws ClassNotFoundException, IOException {
    151     try {
    152       clazz.writeFile();
    153     } catch (NotFoundException e) {
    154       throw new ClassNotFoundException("Error while saving modified class " + clazz.getName(), e);
    155     } catch (CannotCompileException e) {
    156       throw new RuntimeException("Internal Error: Attempt to save syntactically incorrect code "
    157           + "for class " + clazz.getName(), e);
    158     }
    159   }
    160 
    161   CtClass generateInterface(Class<?> originalClass, SdkVersion sdkVersion) {
    162     ClassPool classPool = getClassPool();
    163     try {
    164       return classPool.getCtClass(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
    165     } catch (NotFoundException e) {
    166       CtClass newInterface =
    167           classPool.makeInterface(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
    168       addInterfaceMethods(originalClass, newInterface);
    169       return newInterface;
    170     }
    171   }
    172 
    173   String getInterfaceMethodSource(Method method) throws UnsupportedOperationException {
    174     StringBuilder methodBody = getMethodSignature(method);
    175     methodBody.append(";");
    176     return methodBody.toString();
    177   }
    178 
    179   private StringBuilder getMethodSignature(Method method) {
    180     int modifiers = method.getModifiers();
    181     if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
    182       throw new UnsupportedOperationException(
    183           "Cannot specify final or static methods in an interface");
    184     }
    185     StringBuilder methodSignature = new StringBuilder("public ");
    186     methodSignature.append(getClassName(method.getReturnType()));
    187     methodSignature.append(" ");
    188     methodSignature.append(method.getName());
    189     methodSignature.append("(");
    190     int i = 0;
    191     for (Class<?> arg : method.getParameterTypes()) {
    192       methodSignature.append(getClassName(arg));
    193       methodSignature.append(" arg");
    194       methodSignature.append(i);
    195       if (i < method.getParameterTypes().length - 1) {
    196         methodSignature.append(",");
    197       }
    198       i++;
    199     }
    200     methodSignature.append(")");
    201     if (method.getExceptionTypes().length > 0) {
    202       methodSignature.append(" throws ");
    203     }
    204     i = 0;
    205     for (Class<?> exception : method.getExceptionTypes()) {
    206       methodSignature.append(getClassName(exception));
    207       if (i < method.getExceptionTypes().length - 1) {
    208         methodSignature.append(",");
    209       }
    210       i++;
    211     }
    212     return methodSignature;
    213   }
    214 
    215   private String getClassName(Class<?> clazz) {
    216     return clazz.getCanonicalName();
    217   }
    218 
    219   static ClassPool getClassPool() {
    220     return ClassPool.getDefault();
    221   }
    222 
    223   private boolean classExists(String name) {
    224     // The following line is the ideal, but doesn't work (bug in library).
    225     // return getClassPool().find(name) != null;
    226     try {
    227       getClassPool().get(name);
    228       return true;
    229     } catch (NotFoundException e) {
    230       return false;
    231     }
    232   }
    233 
    234   CtClass generateSubClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
    235       throws ClassNotFoundException {
    236     if (classExists(FileUtils.getSubclassNameFor(superClass, sdkVersion))) {
    237       try {
    238         return getClassPool().get(FileUtils.getSubclassNameFor(superClass, sdkVersion));
    239       } catch (NotFoundException e) {
    240         throw new ClassNotFoundException("This should be impossible, since we just checked for "
    241             + "the existence of the class being created", e);
    242       }
    243     }
    244     CtClass newClass = generateSkeletalClass(superClass, newInterface, sdkVersion);
    245     if (!newClass.isFrozen()) {
    246       newClass.addInterface(newInterface);
    247       try {
    248         newClass.addInterface(getClassPool().get(MockObject.class.getName()));
    249       } catch (NotFoundException e) {
    250         throw new ClassNotFoundException("Could not find " + MockObject.class.getName(), e);
    251       }
    252       addMethods(superClass, newClass);
    253       addGetDelegateMethod(newClass);
    254       addSetDelegateMethod(newClass, newInterface);
    255       addConstructors(newClass, superClass);
    256     }
    257     return newClass;
    258   }
    259 
    260   private void addConstructors(CtClass clazz, Class<?> superClass) throws ClassNotFoundException {
    261     CtClass superCtClass = getCtClassForClass(superClass);
    262 
    263     CtConstructor[] constructors = superCtClass.getDeclaredConstructors();
    264     for (CtConstructor constructor : constructors) {
    265       int modifiers = constructor.getModifiers();
    266       if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) {
    267          CtConstructor ctConstructor;
    268         try {
    269           ctConstructor = CtNewConstructor.make(constructor.getParameterTypes(),
    270                constructor.getExceptionTypes(), clazz);
    271           clazz.addConstructor(ctConstructor);
    272         } catch (CannotCompileException e) {
    273           throw new RuntimeException("Internal Error - Could not add constructors.", e);
    274         } catch (NotFoundException e) {
    275           throw new RuntimeException("Internal Error - Constructor suddenly could not be found", e);
    276         }
    277       }
    278     }
    279   }
    280 
    281   CtClass getCtClassForClass(Class<?> clazz) throws ClassNotFoundException {
    282     ClassPool classPool = getClassPool();
    283     try {
    284       return classPool.get(clazz.getName());
    285     } catch (NotFoundException e) {
    286       throw new ClassNotFoundException("Class not found when finding the class to be mocked: "
    287           + clazz.getName(), e);
    288     }
    289   }
    290 
    291   private void addSetDelegateMethod(CtClass clazz, CtClass newInterface) {
    292     try {
    293       clazz.addMethod(CtMethod.make(getSetDelegateMethodSource(newInterface), clazz));
    294     } catch (CannotCompileException e) {
    295       throw new RuntimeException("Internal error while creating the setDelegate() method", e);
    296     }
    297   }
    298 
    299   String getSetDelegateMethodSource(CtClass newInterface) {
    300     return "public void setDelegate___AndroidMock(" + newInterface.getName() + " obj) { this."
    301         + getDelegateFieldName() + " = obj;}";
    302   }
    303 
    304   private void addGetDelegateMethod(CtClass clazz) {
    305     try {
    306       CtMethod newMethod = CtMethod.make(getGetDelegateMethodSource(), clazz);
    307       try {
    308         CtMethod existingMethod = clazz.getMethod(newMethod.getName(), newMethod.getSignature());
    309         clazz.removeMethod(existingMethod);
    310       } catch (NotFoundException e) {
    311         // expected path... sigh.
    312       }
    313       clazz.addMethod(newMethod);
    314     } catch (CannotCompileException e) {
    315       throw new RuntimeException("Internal error while creating the getDelegate() method", e);
    316     }
    317   }
    318 
    319   private String getGetDelegateMethodSource() {
    320     return "public Object getDelegate___AndroidMock() { return this." + getDelegateFieldName()
    321         + "; }";
    322   }
    323 
    324   String getDelegateFieldName() {
    325     return "delegateMockObject";
    326   }
    327 
    328   void addInterfaceMethods(Class<?> originalClass, CtClass newInterface) {
    329     Method[] methods = getAllMethods(originalClass);
    330     for (Method method : methods) {
    331       try {
    332         if (isMockable(method)) {
    333           CtMethod newMethod = CtMethod.make(getInterfaceMethodSource(method), newInterface);
    334           newInterface.addMethod(newMethod);
    335         }
    336       } catch (UnsupportedOperationException e) {
    337         // Can't handle finals and statics.
    338       } catch (CannotCompileException e) {
    339         throw new RuntimeException(
    340             "Internal error while creating a new Interface method for class "
    341                 + originalClass.getName() + ".  Method name: " + method.getName(), e);
    342       }
    343     }
    344   }
    345 
    346   void addMethods(Class<?> superClass, CtClass newClass) {
    347     Method[] methods = getAllMethods(superClass);
    348     if (newClass.isFrozen()) {
    349       newClass.defrost();
    350     }
    351     List<CtMethod> existingMethods = Arrays.asList(newClass.getDeclaredMethods());
    352     for (Method method : methods) {
    353       try {
    354         if (isMockable(method)) {
    355           CtMethod newMethod = CtMethod.make(getDelegateMethodSource(method), newClass);
    356           if (!existingMethods.contains(newMethod)) {
    357             newClass.addMethod(newMethod);
    358           }
    359         }
    360       } catch (UnsupportedOperationException e) {
    361         // Can't handle finals and statics.
    362       } catch (CannotCompileException e) {
    363         throw new RuntimeException("Internal Error while creating subclass methods for "
    364             + newClass.getName() + " method: " + method.getName(), e);
    365       }
    366     }
    367   }
    368 
    369   Method[] getAllMethods(Class<?> clazz) {
    370     Map<String, Method> methodMap = getAllMethodsMap(clazz);
    371     return methodMap.values().toArray(new Method[0]);
    372   }
    373 
    374   private Map<String, Method> getAllMethodsMap(Class<?> clazz) {
    375     Map<String, Method> methodMap = new HashMap<String, Method>();
    376     Class<?> superClass = clazz.getSuperclass();
    377     if (superClass != null) {
    378       methodMap.putAll(getAllMethodsMap(superClass));
    379     }
    380     List<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods()));
    381     for (Method method : methods) {
    382       String key = method.getName();
    383       for (Class<?> param : method.getParameterTypes()) {
    384         key += param.getCanonicalName();
    385       }
    386       methodMap.put(key, method);
    387     }
    388     return methodMap;
    389   }
    390 
    391   boolean isMockable(Method method) {
    392     if (isForbiddenMethod(method)) {
    393       return false;
    394     }
    395     int modifiers = method.getModifiers();
    396     return !Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers) && !method.isBridge()
    397         && (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers));
    398   }
    399 
    400   boolean isForbiddenMethod(Method method) {
    401     if (method.getName().equals("equals")) {
    402       return method.getParameterTypes().length == 1
    403           && method.getParameterTypes()[0].equals(Object.class);
    404     } else if (method.getName().equals("toString")) {
    405       return method.getParameterTypes().length == 0;
    406     } else if (method.getName().equals("hashCode")) {
    407       return method.getParameterTypes().length == 0;
    408     }
    409     return false;
    410   }
    411 
    412   private String getReturnDefault(Method method) {
    413     Class<?> returnType = method.getReturnType();
    414     if (!returnType.isPrimitive()) {
    415       return "null";
    416     } else if (returnType == Boolean.TYPE) {
    417       return "false";
    418     } else if (returnType == Void.TYPE) {
    419       return "";
    420     } else {
    421       return "(" + returnType.getName() + ")0";
    422     }
    423   }
    424 
    425   String getDelegateMethodSource(Method method) {
    426     StringBuilder methodBody = getMethodSignature(method);
    427     methodBody.append("{");
    428     methodBody.append("if(this.");
    429     methodBody.append(getDelegateFieldName());
    430     methodBody.append("==null){return ");
    431     methodBody.append(getReturnDefault(method));
    432     methodBody.append(";}");
    433     if (!method.getReturnType().equals(Void.TYPE)) {
    434       methodBody.append("return ");
    435     }
    436     methodBody.append("this.");
    437     methodBody.append(getDelegateFieldName());
    438     methodBody.append(".");
    439     methodBody.append(method.getName());
    440     methodBody.append("(");
    441     for (int i = 0; i < method.getParameterTypes().length; ++i) {
    442       methodBody.append("arg");
    443       methodBody.append(i);
    444       if (i < method.getParameterTypes().length - 1) {
    445         methodBody.append(",");
    446       }
    447     }
    448     methodBody.append(");}");
    449     return methodBody.toString();
    450   }
    451 
    452   CtClass generateSkeletalClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
    453       throws ClassNotFoundException {
    454     ClassPool classPool = getClassPool();
    455     CtClass superCtClass = getCtClassForClass(superClass);
    456     String subclassName = FileUtils.getSubclassNameFor(superClass, sdkVersion);
    457 
    458     CtClass newClass;
    459     try {
    460       newClass = classPool.makeClass(subclassName, superCtClass);
    461     } catch (RuntimeException e) {
    462       if (e.getMessage().contains("frozen class")) {
    463         try {
    464           return classPool.get(subclassName);
    465         } catch (NotFoundException ex) {
    466           throw new ClassNotFoundException("Internal Error: could not find class", ex);
    467         }
    468       }
    469       throw e;
    470     }
    471 
    472     try {
    473       newClass.addField(new CtField(newInterface, getDelegateFieldName(), newClass));
    474     } catch (CannotCompileException e) {
    475       throw new RuntimeException("Internal error adding the delegate field to "
    476           + newClass.getName(), e);
    477     }
    478     return newClass;
    479   }
    480 }
    481