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での同じトピックに関する私の講演のビデオ










All Articles