/* * Copyright 2015 LinkedIn Corp. All rights reserved. * * 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. */ package com.linkedin.android.utils; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import java.io.File; import java.io.FileNotFoundException; import java.lang.IllegalAccessException; import java.lang.IllegalArgumentException; import java.lang.NoSuchMethodException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import android.os.Handler; /** * A utility class to help with general Robolectric operations. */ public class TestUtils { private final static Object[] EMPTY_PARAM = new Object[]{}; public static class MockTracker { private List<Object> mockedObjects = new ArrayList<Object>(); public Object get(int position) { return mockedObjects.get(position); } } /** * A convenience method for you to verify mocks returned by {@link com.linkedin.android.utils.TestUtils#mockChildViews(ViewHolder)} for * unwanted interactions. * * @param mocks * - A MockTracker object generated by {@link com.linkedin.android.utils.TestUtils#mockChildViews(ViewHolder)}. */ public static void verifyNoMoreInteraction(MockTracker mocks) { for (Object obj : mocks.mockedObjects) { Mockito.verifyNoMoreInteractions(obj); } } /** * Read the content of the given file. The file must be located within {@code LinkedInApp/tests/resources/} * * @param path * - The path of the file relative to LinkedInApp/tests/resources/ * @return the content of the file as a string * @throws java.io.FileNotFoundException */ public static String readFile(String path) throws FileNotFoundException { String str = ""; str = new Scanner(new File("tests/resources/" + path)).useDelimiter("\\A").next(); return str; } /** * Allows us to query any member field of an object (including a private one). This is useful for inspecting and * verifying the private member variables of an object in a test. Note that if you change a variable name, you'll * no longer get a compile failure for name mismatching, it'll only happen at runtime. This is why this method is * only used in tests (We don't do this style of reflection in the app itself). Example usage might be: * * class Dummy { * private Context context; * } * * Dummy dummy = new Dummy(); * * Context context = TestUtils.getPrivateField(dummy, "context"); * * Return type is based on expected type. If expected type is ambiguous (say for a method that has multiple * implementations with various types), you can specify the exact return type with: * * someFunction(TestUtils.<Context>getPrivateField(dummy, "context"); * * The fieldName is caseInsensitive. If more than one member variable has the same case-insensitive name, the results * are indeterminate. This method will check SuperClasses as well. Ordering always returns the value of the variable * closest to the current class. For instance, if the class of the object passed in has the field, that one will be * retrieved even if the SuperClass also has a field with the same name. If the SuperClass has the field and the * current class does not, the SuperClasses will be used, even if it's superClass also has the field declared. * Also, never use the same variable name with different cases. Seriously. * * @param target object to get a field of * @param fieldName case insensitive name of the field to retrieve the value of * @param <T> type of the field. * @return the value of the field named by fieldName in the passed in target object. * @throws NoSuchFieldException if the field named does not exist. * @throws IllegalAccessException in case it's impossible to access the field due to a permission error. Normally * shouldn't happen. */ @SuppressWarnings("unchecked") public static <T> T getPrivateField(Object target, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field field = getDeclaredFieldIgnoreCase(target.getClass(), fieldName); field.setAccessible(true); return (T) field.get(target); } /** * Go through all of the declaredFields of the given class and all superClasses and return one that matches * the passed in fieldName, ignoring case. * @param clazz is the Class to get declaredFields of * @param fieldName the fieldName to find of the given class * @return a Field object of the given name in the given class if it exists * @throws NoSuchFieldException if that field name does not exist at all */ private static Field getDeclaredFieldIgnoreCase (Class clazz, String fieldName) throws NoSuchFieldException { if (clazz == null) { throw new NoSuchFieldException("Unable to find [" + fieldName + "] in this class or any super classes"); } for (Field field : clazz.getDeclaredFields()) { if (field.getName().equalsIgnoreCase(fieldName)) { return field; } } Class superClass = clazz.getSuperclass(); return getDeclaredFieldIgnoreCase(superClass, fieldName); } @SuppressWarnings("unchecked") public static Object invokePrivateMethod(Object target, String methodName, Object... parameters) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method method = getDeclaredMethodIgnoreCase(target.getClass(), methodName, parameters); method.setAccessible(true); if (parameters != null && parameters.length > 0) { return method.invoke(target, (Object[]) parameters); } return method.invoke(target); } private static Method getDeclaredMethodIgnoreCase(Class clazz, String methodName, Object... parameters) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (clazz == null) { throw new NoSuchMethodException("Unable to find [" + methodName + "] in this class or any super classes"); } for (Method method : clazz.getDeclaredMethods()) { // name match if (!method.getName().equalsIgnoreCase(methodName)) { continue; } // length match - null == 0 Class[] types = method.getParameterTypes(); boolean parametersIsEmpty = parameters == null || parameters.length == 0; boolean typesIsEmpty = types == null || types.length == 0; if ((parametersIsEmpty != typesIsEmpty) || (parameters.length != types.length)) { continue; } // isInstance match if (!typesIsEmpty) { boolean incompatibleTypeFound = false; for (int i = 0; i < types.length; i++) { if (!types[i].isInstance(parameters[i]) && parameters[i] != null){ incompatibleTypeFound = true; break; } } if (incompatibleTypeFound) { continue; } } return method; } return getDeclaredMethodIgnoreCase(clazz.getSuperclass(), methodName, parameters); } /** * Allows us to set the value of a member variable of an object (even a private one) using reflection. This method * is ok to use in test cases but this type of reflection should never be used in the main app. Note that if you * change a variable name, you'll no longer get a compile failure for name mismatching, it'll only happen at runtime. * Example of usage might be: * * class Dummy { * private Context context; * } * * Dummy dummy = new Dummy(); * Context context = new Context(); * * TestUtils.setPrivateField(dummy, "context", context); * * The set fieldName is case insensitive. This method will indeterminately set the value to the first fieldName * that matches (ordering isn't guaranteed). If a field is not found in the current class, the superClass will also * be checked recursively until all SuperClasses are checked for the field. This will be a problem if you have 2 or * more member variables with the same name of different cases, which you really shouldn't be doing. Seriously. * * @param target object to set the value of a member variable * @param fieldName is the name of the member variable you will be setting * @param fieldValue is the value of that member variable that you want set * @throws NoSuchFieldException if the fieldName is not a memberVariable of the given target object * @throws IllegalAccessException in case it's impossible to access the field due to a permission error. Normally * shouldn't happen. */ public static void setPrivateField(Object target, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Field field = getDeclaredFieldIgnoreCase(target.getClass(), fieldName); field.setAccessible(true); field.set(target, fieldValue); } /** * Helper method to immediately run all posted runnables on handler in order, * bypassing delays. * * @param handler whose post() calls to execute */ public static void mockRunOnHandler(Handler handler) { doAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Runnable runnable = (Runnable) invocation.getArguments()[0]; runnable.run(); return null; } }).when(handler).postDelayed(any(Runnable.class), any(Long.class)); doAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Runnable runnable = (Runnable) invocation.getArguments()[0]; runnable.run(); return null; } }).when(handler).post(any(Runnable.class)); } /** * Helper to invoke the private method having primitive arguments * * @param target * The class on which the method is to be invoked * @param methodName * The methodName to be invoked * @param parameterTypes * The paramter types. For Ex: If we need to call a function with * int and String as paramter, we need to specify this as new * Class[] {int.class, String.class} * @param values * The parameter values * @return The return type of the function, if any * @throws Exception */ public static Object invokePrivateMethodForPrimitiveArgs(Object target, String methodName, Class<?>[] parameterTypes, Object... values) throws Exception { Method method = target.getClass().getDeclaredMethod(methodName, parameterTypes); method.setAccessible(true); return method.invoke(target, values); } }