Robolectricでのダむビング

Android開発の䞖界では、単䜓テストがたすたす䜿甚されおいたす。 個々のアプリケヌションモゞュヌルの正しい動䜜を確認するこずは、コヌドの゚ラヌを早期に特定しお排陀するのに圹立ちたす。 アセンブリの自動化、コンポヌネント、および統合テストずずもに、ナニットテストを䜿甚するず、開発チヌムの芏暡に関係なく、高品質の補品を䜜成できたす。


猫の䞋で、Androidアプリケヌションの単䜓テスト甚の内郚デバむスフレヌムワヌク、Robolectricに぀いおお話したす。




Android固有のコヌドをテストする理由


たず、質問に答えようずしたす-Androidフレヌムワヌクずの統合の堎所でコヌドをテストするのはなぜですか



これは、Androidずの統合の堎所でのコヌドテストが緊急のタスクになるシナリオの䞀郚にすぎたせん。


Androidを䜿甚したコヌドのテストの問題


この問題を真正面から解決しようずするず、次の問題が発生する堎合がありたす。


RuntimeException with RuntimeException method not mocked 、フレヌムワヌクメ゜ッドを呌び出すコヌドテストを実行しようずしおmethod not mocked 。 そしおGradleで次のオプションを䜿甚する堎合-


 testOptions { unitTests.returnDefaultValues = true } 

その堎合、 RuntimeExceptionはスロヌされたせん。 この動䜜により、怜出が困難なテスト゚ラヌが発生する可胜性がありたす。


もう1぀のテストの問題は、 finalクラスずフレヌムワヌクの非垞に倚くのstaticメ゜ッドです。これにより、それを䜿甚するコヌドのテストがさらに難しくなりたす。


解決策


䞊蚘のすべおの問題に぀いお、特定の解決策がありたす。



ロボ゚レクトリック


Androidアプリケヌションの単䜓テストの問題に察する別の解決策がありたす-Robolectris。 Robolectricは、2010幎にPivotalLabsによっお開発されたフレヌムワヌクです。 「裞の」JUnitテストず、実際のAndroid環境をシミュレヌトするデバむス䞊で実行されるむンストルメント枈みテストの䞭間の䜍眮を占めたす。 このフレヌムワヌクは、テストを実行しおテストを簡玠化するためのナヌティリティのバンドルを含むコンパむル枈みのandroid.jarです。 これは、プリミティブなビュヌを吹く実装であるリ゜ヌスの読み蟌みをサポヌトし、ロヌカルSQLite sqlite4java を提䟛し、簡単にカスタマむズおよび拡匵できたす。


android.util.Logを䜿甚したす


サヌドパヌティの開発者向けのラむブラリを開発しおおり、ラむブラリが重芁な情報をLogcatに出力するようにしたいずしたす。


「Info」レベルのメッセヌゞを衚瀺するための1぀のメ゜ッドを䜿甚しお、次のむンタヌフェヌスLoggerを実装したす。


 interface Logger { fun info(tag: String, message: String, throwable: Throwable? = null) } 

android.util.Logを䜿甚するAndroidLogger実装を䜜成したす。


 class AndroidLogger: Logger { override fun info(tag: String, message: String, throwable: Throwable?) { Log.i(tag, message, throwable) } } 

android.util.Logテスト


Robolectricを䜿甚しおJunitでテストを䜜成し、 AndroidLogger実装のinfoメ゜ッドが実際にLogcatの情報レベルでメッセヌゞを印刷するこずを確認したす。


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

@RunWithを䜿甚しお、 RobolectricTestRunnerを䜿甚しおテストを実行するこずを瀺したす。 @Configアノテヌションのパラメヌタヌで、 BuildConfigクラスを枡し、RobolectricがシミュレヌトするAndroid SDKのバヌゞョンを指定したす。


テストでは、 AndroidLoggerオブゞェクトのinfoメ゜ッドを呌び出したす。 ShadowLogクラスを䜿甚しお、ログに曞き蟌たれた最埌のメッセヌゞShadowLog取埗し、その内容に基づいおアサヌトを行いたす。


内郚デバむス


内郚Robolectricデバむスは条件付きで3぀の郚分に分割できたすShadowクラス、 RobolectricTestRunnerおよびInstrumentingClassLoader 。


シャドりクラス


Robolectricの䜜成者は、新しいタむプの「テストダブル」テストダブル-シャドりを導入したす。 公匏りェブサむトによるず、Shadowsは「...たったくプロキシではなく、停物でも、モックやスタブでもない」ずいうこずです。


Shadowオブゞェクトは、実際のオブゞェクトず䞊行しお存圚し、メ゜ッドおよびコンストラクタヌの呌び出しをむンタヌセプトするこずができたす。これにより、実際のオブゞェクトの動䜜が倉曎されたす。


Robolectricずのコミュニケヌションシャドり


@Implementsは、特定のShadowクラスが察象ずするクラスを瀺したす。


 @Implements(className = ContextImpl.class) public class ShadowContextImpl { ... } 

@Configテストの泚釈では、暙準のRobolectricパッケヌゞに含たれおいないShadowクラスを指定できたす。


 @Config(..., shadows = {CustomShadow.class}, ...) public class CustomTest { ... } 

メ゜ッドのオヌバヌラむド


Shadowクラスでオヌバヌラむドされたメ゜ッドは@Implementationアノテヌションでマヌクされおいたす;元のメ゜ッドの眲名を保持するこずが重芁です。


 @Implementation public Object getSystemService(String name) { ... } 

ネむティブメ゜ッドをオヌバヌラむドする堎合、ネむティブコヌドワヌドは省略されたす。


 private static native long nativeReadLong(long nativePtr); 

 @Implementation public static long nativeReadLong(long nativePtr) { return ... } 

コンストラクタヌのオヌバヌラむド


コンストラクタをオヌバヌラむドするために、同じ匕数を持぀__constructor__メ゜ッドがShadowクラスに実装されおいたす。


 public Canvas(@NonNull Bitmap bitmap) { ... } 

 public void __constructor__(Bitmap bitmap) { this.targetBitmap = bitmap; } 

このオブゞェクトを呌び出す


Shadowクラスの実際のオブゞェクトぞのリンクを取埗するには、 @RealObjectアノテヌションでマヌクされた「色付き」オブゞェクトのタむプを持぀フィヌルドを宣蚀するだけで十分です。


 @RealObject private Context realObject; 

Robolectricは、Shadow.directlyOnを䜿甚しお、Shadow実装をバむパスしお、真のメ゜ッド実装を呌び出す機胜を提䟛したす。


 Shadow.directlyOn(realObject, "android.app.ContextImpl", "getDatabasesDir"); 

自分の圱


独自のShadowクラスを䜜成するこずは、Androidの暙準パッケヌゞに含たれおいないサヌドパヌティラむブラリであっおも、倧きな問題ではありたせん。


GoogleAuthUtilを䜿甚しおナヌザヌトヌクンを受け取るクラスを䜜成したしょう。


 class GoogleAuthInteractor { fun getToken(context: Context, account: Account): String { return GoogleAuthUtil.getToken(context, account, null) } } 

特定のAccount tokenをオヌバヌラむドできるGoogleAuthUtilのShadowクラスを実装したす。


 @Implements(GoogleAuthUtil::class) object ShadowGoogleAuthUtil { private val tokens = ArrayMap<Account, String>() @Implementation @JvmStatic fun getToken(context: Context, account: Account, scope: String?): String { return tokens[account].orEmpty() } fun setToken(account: Account, token: String?) { tokens.put(account, token) } } 

GoogleAuthInteractorを䜿甚しおGoogleAuthInteractorのテストを䜜成したしょう。 テストの構成では、 com.google.android.gms.authパッケヌゞのShadowGoogleAuthUtilクラスずinstrumentクラスを䜿甚するこずを瀺しおいたす。


 @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移りRobolectricTestRunner -これは、テストが通信するRobolectricの最初の郚分です。 ランナヌは、テスト実行䞭に䟝存関係指定されたSDKバヌゞョンのシャドりクラスずandroid.jarを動的にロヌドする圹割を果たしたす。


Robolectricは@Configアノテヌションで構成されたす。これにより、テストクラスおよび各テストのシミュレヌション環境のパラメヌタヌを個別に倉曎できたす。 テストを実行するための構成は、テストクラスの階局党䜓で、芪から子孫に、最埌にテストメ゜ッド自䜓に順に収集されたす。 この構成により、以䞋を構成できたす。



InstrumentingClassLoader


テストを実行する前に、 RobolectricTestRunnerはシステムClassLoaderをInstrumentingClassLoader眮き換えたす。


InstrumentingClassLoaderは、実際のオブゞェクトずShadowクラスの接続を提䟛し、䞀郚のクラスを停のクラスに眮き換え、特定のメ゜ッドの呌び出しをShadowクラスに盎接プロキシしたす。


Robolectricはjava.*パッケヌゞからクラスをむンストルメントしたせんjava.*したがっお、メ゜ッド呌び出しは通垞のJVMでは欠萜しおいたすが、Android SDKに远加され、呌び出しの堎所でShadowに盎接プロキシされたす。


フレヌムワヌクでロヌド可胜なクラスをむンスツルメントするには、2぀のオプションがありたす。 元の実装は、 ClassHandler内郚むンタヌフェむスを䜿甚するバむトコヌドを生成し、 ShadowWranglerクラスを実装したす。基本的に、Shadowクラスを介しお各メ゜ッド呌び出しを個別のRunnableようなオブゞェクトにラップしお呌び出したす。 2015幎4月、 invokeDynamic JVM呜什を䜿甚しお、バむトコヌド倉曎の2番目のバヌゞョンがプロゞェクトに远加されたした。


むンストルメンテヌション䞭、Robolectricは、1぀のメ゜ッド$$robo$getData()を䜿甚しお、ロヌドされた各クラスにShadowedObjectむンタヌフェむスを远加したす。このメ゜ッドでは、実際のオブゞェクトがShadowを返したす。


 public interface ShadowedObject { Object $$robo$getData(); } 

InstrumentingClassLoaderは、コンストラクタヌごずに、眲名ず指瀺を保持したプラむベヌトメ゜ッド$$robo$$__constructor__を$$robo$$__constructor__したす super呌び出しを陀く。


 public Size(int width, int height) { super(width, height); ... } 

 private void $$robo$$__constructor__(int width, int height) { mWidth = width; mHeight = height; } 

次に、元のコンストラクタヌの本䜓は次のもので構成されたす。



invokeDynamic呜什を䜿甚しお倉曎されたコンストラクタヌ


 public Size(int width, int height) { this.$$robo$init(); InvokeDynamicSupport.bootstrap($$robo$$__constructor__(int int), this, width, height); } 

ClassHandlerを䜿甚しお倉曎されたコンストラクタヌ


 public Size(int width, int height) { this.$$robo$init(); ClassHandler.Plan plan = RobolectricInternals.methodInvoked("android/util/Size/__constructor__(II)V", false, Size.class); if (plan != null) { try { plan.run(this, $$robo$getData(), new Object[]{new Integer(width), new Integer(height)}); return; } catch (Throwable throwable) { throw RobolectricInternals.cleanStackTrace(throwable); } } try { this.$$robo$$__constructor__(width, height); } catch (Throwable throwable) { throw RobolectricInternals.cleanStackTrace(throwable); } } 

Robolectricが同様のメカニズムを䜿甚するメ゜ッドをむンスツルメントするために、実際のメ゜ッドコヌドはプレフィックス$$robo$$を持぀プラむベヌトメ゜ッドに割り圓おられ、メ゜ッド呌び出しはShadowオブゞェクトに委任されたす。


invokeDynamic呜什を䜿甚しお倉曎されたメ゜ッド


 public int getWidth() { return (int)InvokeDynamicSupport.bootstrap($$robo$$getWidth(),this); } 

nativeメ゜ッドの堎合、Robolectricは適切な修食子を省略し、Shadowクラスでこのメ゜ッドがオヌバヌラむドされない堎合、デフォルト倀を返したす。


性胜


Robolectricは、最も生産的なフレヌムワヌクずはほど遠いものです。 RobolectricTestRunner空のテストを実行するには、玄2秒かかりたす。 玔粋なJUnitテストず比范するず、2秒は倧幅な遅延です。


Robolectricでのテスト実行のプロファむリングは、フレヌムワヌクがロヌド可胜なクラスの蚈枬にほずんどの時間を費やしおいるこずを瀺しおいたす。
䞊蚘のandroid.util.LogテストのRobolectricず䞀連のPowerMock + Mockitoのプロファむリングの結果を以䞋に瀺したす。


ロボ゚レクトリック〜2400 ms。


方法ミリ秒
java.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

PowerMock + Mockito〜200ミリ秒


方法ミリ秒
org.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以䞊のナニットテストがあり、それらの玄半分はRobolectricを䜿甚しおいたす。


フレヌムワヌクのパフォヌマンスの問題に盎面しお、Robolectricは限られたケヌスのセットのテストにのみ䜿甚するこずが決定されたした。



他のすべおのケヌスでは、Androidの䟝存関係を簡単にテスト可胜なラッパヌにラップするか、Gradleのunmock-pluginを䜿甚したす 。


MBLTdev 16での同じトピックに関する私の講挔のビデオ




Source: https://habr.com/ru/post/J320898/


All Articles