"Погружение в Robolectric" Дмитрий Костырев (Avito)

34
Погружение в Robolectric Костырев Дмитрий Avito

Transcript of "Погружение в Robolectric" Дмитрий Костырев (Avito)

Погружение в 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● Pivotal Labs 2010

● Поддержан Android API 23 (Marshmallow)

5

Возможности 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 объекты

● RobolectricTestRunner

● InstrumentingClassLoader

9

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")) }}

Внутреннее устройство● Shadow объекты

➔ RobolectricTestRunner

● InstrumentingClassLoader

18

Конфигурация● @Config для классов и методов

● Версия API

● Квалификаторы для ресурсов

● Дополнительные Shadows

● Пакеты для инструментирования

19

RobolectricTestRunner● AndroidManifest.xml + /res + /assets

● Загрузка android.jar из внешнего репозитория

● Замена ClassLoader

● Запуск тестов

20

Внутреннее устройство● Shadow

● RobolectricTestRunner

➔ InstrumentingClassLoader

21

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

Спасибо за внимание!Вопросы?