Android開発の世界では、単体テストがますます使用されています。 個々のアプリケーションモジュールの正しい動作を確認することは、コードのエラーを早期に特定して排除するのに役立ちます。 アセンブリの自動化、コンポーネント、および統合テストとともに、ユニットテストを使用すると、開発チームの規模に関係なく、高品質の製品を作成できます。
猫の下で、Androidアプリケーションの単体テスト用の内部デバイスフレームワーク、Robolectricについてお話します。

Android固有のコードをテストする理由
まず、質問に答えようとします-Androidフレームワークとの統合の場所でコードをテストするのはなぜですか?
リソース-特定の文字列または他のアプリケーションリソースの正しい使用をテストする価値があります。 これらはビジネス要件の不可欠な部分です。
Parcelable-Parcelableの自動生成手段を使用するか実装を手動で記述するかに関係なく、シリアル化された表現からオブジェクトを復元する正確性をテストする価値があります。
SQLite-データ移行のテスト、スキーマの変更、新しいテーブルの追加、クエリの実行の修正。
インテント/バンドル-いくつかのシナリオでは、次のアクティビティまたはサービスが起動されるフラグであるインテントの充填の正確性を確認することが重要です。
- カメラ、MediaPlayer、MediaRecorder、さまざまなマネージャーなどの非UIシステムコンポーネント
これは、Androidとの統合の場所でのコードテストが緊急のタスクになるシナリオの一部にすぎません。
Androidを使用したコードのテストの問題
この問題を真正面から解決しようとすると、次の問題が発生する場合があります。
RuntimeException
with RuntimeException
method not mocked
、フレームワークメソッドを呼び出すコードテストを実行しようとしてmethod not mocked
。 そしてGradleで次のオプションを使用する場合-
testOptions { unitTests.returnDefaultValues = true }
その場合、 RuntimeException
はスローされません。 この動作により、検出が困難なテストエラーが発生する可能性があります。
もう1つのテストの問題は、 final
クラスとフレームワークの非常に多くのstatic
メソッドです。これにより、それを使用するコードのテストがさらに難しくなります。
解決策
上記のすべての問題について、特定の解決策があります。
コードがフレームワークと統合される場所では、プリミティブテストラッパーを使用します。 テストでは、ラッパーを濡らし、コードとの相互作用をテストします。 単純な実装を考慮したラッパーのテストは省略されています。 実際、このラッパーはテストする必要がありますが、短時間はプリミティブのままです。 最終的に、テストのためにAndroidフレームワークの実装を複製するのにうんざりします。 APK内のメソッドの数が増えることを忘れないでください。これはこのアプローチにつながります。
インスツルメントされた単体テストは、最も正確なテストオプションです。 テストは、実際の環境の実際のデバイスまたはエミュレーターで実行されます。 ただし、長いコンパイル、APKのパッキング、およびテストの実行に時間がかかるため、この費用を支払う必要があります。
- PowerMock + Mockito-PowerMockを使用すると、
static
メソッドとfinal
クラスをフックできます。 この場合、一部のAndroidクラスの動作を部分的に繰り返す必要があります。これにより、テストでmoxaを準備するコードが膨張し、将来サポートが難しくなる可能性があります。
ロボエレクトリック
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
アノテーションで構成されます。これにより、テストクラスおよび各テストのシミュレーション環境のパラメーターを個別に変更できます。 テストを実行するための構成は、テストクラスの階層全体で、親から子孫に、最後にテストメソッド自体に順に収集されます。 この構成により、以下を構成できます。
- Android版
- マニフェストとリソースパス
- 現在の修飾子のリスト
- サードパーティのシャドウ
- 計装用の追加パッケージ名
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; }
次に、元のコンストラクターの本体は次のもので構成されます。
-
super
コール(クラスが継承者の場合) - プライベートフィールド
__robo_data__
を対応するShadowオブジェクトで初期化$$robo$init
プライベートメソッド$$robo$init
__robo_data__
$$robo$init
- Shadowオブジェクトが存在し、対応するコンストラクターが再定義されている場合、Shadowオブジェクトでオーバーライドされたコンストラクター(
__constructor__
)を呼び出します。そうでない場合、実際の実装が呼び出されます($$robo$$__constructor__
)。
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.
| 767 |
org.objectweb.asm.tree.ClassNode.accept(ClassVisitor)
| 407 |
org.objectweb.asm.tree.MethodNode.accept(ClassVisitor)
| 367 |
org.robolectric.internal.bytecode.InstrumentingClassLoader
| 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
| 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は限られたケースのセットのテストにのみ使用することが決定されました。
- 小包
- リソース内の文字列のフォーマット
- 非UIコンポーネント(カメラ)
他のすべてのケースでは、Androidの依存関係を簡単にテスト可能なラッパーにラップするか、Gradleのunmock-pluginを使用します 。
MBLTdev 16での同じトピックに関する私の講演のビデオ