Test Drive Android with Robolectric - Atlanta Android Meetup
"Погружение в Robolectric" Дмитрий Костырев (Avito)
Transcript of "Погружение в Robolectric" Дмитрий Костырев (Avito)
● Resources
● Parcelable
● SQLite
● Intent / Bundle
● Не UI компоненты Android (Camera, MediaPlayer, …)
● … ?
Зачем тестировать Android специфичный код?
2
● RuntimeException Method … not mocked
Проблемы тестирования Android кода
testOptions { unitTests.returnDefaultValues = true}
● final классы / static методы
● Низкая тестируемость фреймворка
3
● Примитивные обертки без логики и тестов
Пути решения
4
● Instrumentation тесты
● PowerMock / Mockito
Возможности Robolectric● android.jar для локальной JVM + вспомогательные утилиты
● Загрузка ресурсов
● View Inflation *
● SQLite (sqlite4java)
● Кастомизируем и расширяем
6
Используем android.util.Log
interface Logger { fun info(tag: String, message: String, throwable: Throwable? = null)}
class AndroidLogger: Logger { override fun info(tag: String, message: String, throwable: Throwable?) { Log.i(tag, message, throwable) }}
7
Тестируем android.util.Log@RunWith(RobolectricTestRunner::class)@Config(constants = BuildConfig::class, sdk = intArrayOf(23))class RobolectricAndroidLoggerTest {
private val logger: Logger = AndroidLogger()
@Test fun `info - should log to logcat with info level`() { val throwable = Throwable()
logger.info("Tag", "Message", throwable)
val logInfo: LogInfo = ShadowLog.getLogs().last() assertThat(logInfo.type, Is(Log.INFO)) assertThat(logInfo.tag, Is("Tag")) assertThat(logInfo.msg, Is("Message")) assertThat(logInfo.throwable, Is(throwable)) }}
8
Shadow объекты● Test Double
● “… not quite Proxies, not quite Fakes, not quite Mocks or Stubs”
● Существуют параллельно реальному объекту
● Могут перехватить вызовы методов / конструкторов
10
Связь Shadow c Robolectric
@Implements(className = ContextImpl.class)public class ShadowContextImpl { ...}
11
@Config(..., shadows = {CustomShadow.class}, ...)public class CustomTest { ...}
Переопределение методов
@Implementationpublic static long nativeReadLong(long nativePtr) { return ...}
private static native long nativeReadLong(long nativePtr);
12
@Implementationpublic Object getSystemService(String name) { ...}
Переопределение конструкторов
public void __constructor__(Bitmap bitmap) { this.targetBitmap = bitmap;}
public Canvas(@NonNull Bitmap bitmap) { ...}
13
Вызов настоящего объекта
Shadow.directlyOn(realObject, "android.app.ContextImpl", "getDatabasesDir");
@RealObjectprivate Context realObject;
14
Используем GoogleAuthUtil
15
class GoogleAuthInteractor { fun getToken(context: Context, account: Account): String { return GoogleAuthUtil.getToken(context, account, null) }}
ShadowGoogleAuthUtil
16
@Implements(GoogleAuthUtil.class)public class ShadowGoogleAuthUtil {
private static final Map<Account, String> tokens = new HashMap<>();
@Implementation public static String getToken(Context context, Account account, String scope) { return tokens.get(account); }
public static void setToken(Account account, String token) { tokens.put(account, token); }}
Тестируем GoogleAuthUtil
17
@RunWith(RobolectricTestRunner::class)@Config(shadows = arrayOf(ShadowGoogleAuthUtil::class), instrumentedPackages = arrayOf("com.google.android.gms.auth"))class GoogleAuthInteractorTest {
private val context = RuntimeEnvironment.application private val interactor = GoogleAuthInteractor()
@Test fun `provide token - provides token for correct account`() { val account = Account("name", "type") ShadowGoogleAuthUtil.setToken(account, "token")
val token = interactor.getToken(context, account)
assertThat(token, Is("token")) }}
Конфигурация● @Config для классов и методов
● Версия API
● Квалификаторы для ресурсов
● Дополнительные Shadows
● Пакеты для инструментирования
19
RobolectricTestRunner● AndroidManifest.xml + /res + /assets
● Загрузка android.jar из внешнего репозитория
● Замена ClassLoader
● Запуск тестов
20
InstrumentingClassLoader● Связывает реальный объект с Shadow
● Подменяет class name некоторых классов на class name фейков
● Проксирует вызовы некоторых методов
22
Проксирование методовjava.util.LinkedHashMap.eldest
java.util.Locale.adjustLanguageCode
java.lang.System.loadLibrary
java.lang.System.arraycopy
java.lang.System.nanoTime
java.lang.System.currentTimeMillis
java.lang.System.logE
...23
Связь с Shadow● 2 реализации вызова методов на Shadow объектах:
● Использование ShadowWrangler
● С помощью инструкции invokedynamic (JDK > 1.8, опционально)
24
Связь с Shadow
public interface ShadowedObject { Object $$robo$getData();}
public class ShadowExtractor { public static Object extract(Object instance) { return ((ShadowedObject) instance).$$robo$getData(); }}
25
Модификация конструкторов
private void $$robo$$__constructor__(int width, int height) { mWidth = width; mHeight = height;}
public Size(int width, int height) { super(width, height); ...}
26
Модификация конструкторов
protected void $$robo$init() { if (__robo_data__ != null) return; __robo_data__ = RobolectricInternals.initializing((Object)this);
…
__robo_data__ = InvokeDynamicSupport.bootstrapInit(this);}
27
Модификация конструкторов
public Size(int width, int height) { super(width, height); ... $$robo$init(); // Существует ли shadow объект и “переопределен” __constructor__? // Если да, то вызвать __constructor__(width, height) на shadow объекте
// В противном случае $$robo$$__constructor__(width, height);}
28
Модификация методов
private int $$robo$$getWidth() { return mWidth;}
public int getWidth() { // если shadow объект instanceOf текущего класса - вызвать метод напрямую // существует ли shadow и “переопределен” метод? - вызвать метод
// В противном случае $$robo$$getWidth();}
29
Модификация native методов
private static native int nativeDataSize(long nativePtr);
private static int nativeDataSize(long nativePtr) { ...}
private static int $$robo$$nativeDataSize(long nativePtr) { return 0;}
30
ПроизводительностьRobolectric ~ 2 400 msjava.lang.ClassLoader.loadClass(String) 913
org.robolectric.internal.bytecode.InstrumentingClassLoader.getInstrumentedBytes(ClassNode, boolean)
767
org.objectweb.asm.tree.ClassNode.accept(ClassVisitor) 407
org.objectweb.asm.tree.MethodNode.accept(ClassVisitor) 367
org.robolectric.internal.bytecode.InstrumentingClassLoader$ClassInstrumentor.instrument() 298
org.objectweb.asm.ClassReader.accept(ClassVisitor, Attribute[], int) 277
org.robolectric.shadows.ShadowResources.getSystem() 268
31
PowerMock + Mockito ~200 msorg.powermock.api.extension.proxyframework.ProxyFrameworkImpl.isProxy(Class) 304
org.powermock.api.mockito.repackaged.cglib.core.KeyFactory$Generator.generateClass(ClassVisitor)
131
sun.launcher.LauncherHelper.checkAndLoadMain(boolean, int, String) 103
javassist.bytecode.MethodInfo.rebuildStackMap(ClassPool) 85
java.lang.Class.getResource(String) 84
org.mockito.internal.MockitoCore.<init>() 67
Опыт использования● Более 3000 Unit тестов (~1500 используют Robolectric)
● Parcelable
● Resources (строки, форматирование)
● Тестирование не UI компонент (Camera)
32
Альтернативы● Unmock-plugin для Gradle (https://github.com/bjoernQ/unmock-plugin)
● Абстрагировать вызовы к Android
33