Androidテストチュートリアル:真のグリーンドロイドのようなユニットテスト

公開: 2022-03-11

経験豊富なアプリ開発者として、私たちが開発するアプリケーションが成熟するにつれて、テストを開始する時期であると直感します。 多くの場合、ビジネスルールは、システムがさまざまなリリースを通じて安定性を提供する必要があることを示唆しています。 また、理想的には、ビルドプロセスを自動化し、アプリケーションを自動的に公開したいと考えています。 このため、ビルドが期待どおりに機能していることを保証するために、Adnroidテストツールを配置する必要があります。

テストは、私たちが構築するものについてのさらなるレベルの信頼を提供することができます。 完璧でバグのない製品を作成することは(不可能ではないにしても)困難です。 したがって、私たちの目標は、アプリケーションに新たに導入されたバグをすばやく見つけるテストスイートを設定することにより、市場で成功する可能性を高めることです。

Androidテストチュートリアル

Android、および一般的なさまざまなモバイルプラットフォームに関しては、アプリのテストが課題になる可能性があります。 単体テストを実装し、テスト駆動開発などの原則に従うことは、少なくとも直感的ではないと感じることがよくあります。 それにもかかわらず、テストは重要であり、当然のことと見なしたり無視したりするべきではありません。 David、Kent、Martinは、「TDDは死んでいますか?」というタイトルの記事にまとめられた、テストの利点と落とし穴について、彼らの間の一連の会話で議論しました。 また、そこで実際のビデオ会話を見つけて、テストが開発プロセスに適合しているかどうか、そして今からどの程度組み込むことができるかについて、より多くの洞察を得ることができます。

このAndroidテストのチュートリアルでは、Androidでのユニットと受け入れ、回帰テストについて説明します。 Androidでのテスト単位の抽象化に焦点を当て、その後、開発者とQAのフィードバックサイクルを短縮するためにプロセスを可能な限り高速かつシンプルにすることに焦点を当てて、受け入れテストの例を示します。

私はそれを読むべきですか?

このチュートリアルでは、Androidアプリケーションのテストに関してさまざまな可能性を探ります。 Androidプラットフォームの現在のテストの可能性をよりよく理解したい開発者またはプロジェクトマネージャーは、この記事に記載されているアプローチのいずれかを採用したい場合は、このチュートリアルを使用することを決定できます。 ただし、これは特効薬ではありません。このようなトピックに関連する議論は、期限、コードベースのコード品質、システムの結合レベル、アーキテクチャ設計における開発者の好み、機能の予測寿命とともに、製品ごとに本質的に異なります。テストなど。

単位で考える:Androidテスト

理想的には、アーキテクチャの1つの論理ユニット/コンポーネントを個別にテストする必要があります。 このようにして、コンポーネントが期待する一連の入力に対して適切に機能することを保証できます。 依存関係をモックすることができます。これにより、高速に実行されるテストを作成できます。 さらに、テストに提供された入力に基づいてさまざまなシステム状態をシミュレートし、プロセスのエキゾチックなケースをカバーすることができます。

Androidの単体テストの目標は、プログラムの各部分を分離し、個々の部分が正しいことを示すことです。 単体テストは、コードの一部が満たさなければならない厳密な書面による契約を提供します。 結果として、それはいくつかの利点をもたらします。 —ウィキペディア

Robolectric

Robolectricは、開発ワークステーションのJVM内でテストを実行できるAndroidユニットテストフレームワークです。 Robolectricは、ロード中にAndroid SDKクラスを書き換え、通常のJVMで実行できるようにするため、テスト時間が短縮されます。 さらに、ビューのインフレーション、リソースの読み込みなど、AndroidデバイスのネイティブCコードに実装されているものを処理するため、自動テストを実行するためのエミュレーターと物理デバイスの必要性がなくなります。

モッキート

Mockitoは、Javaでクリーンなテストを作成できるようにするモックフレームワークです。 これにより、本番環境で使用されるコンポーネント/モジュールの元の依存関係を置き換えるために使用されるテストダブル(モック)を作成するプロセスが簡素化されます。 StackOverflowの回答では、モックとスタブの違いについて、詳細を読むために読むことができる非常に簡単な用語で説明しています。

 // you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));

さらに、Mockitoを使用すると、メソッドが呼び出されたかどうかを確認できます。

 // mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear(); 

Testdroid

これで、モックされたオブジェクト/コンポーネントに対して特定のアクションを実行したときに何が起こるかを定義するアクションとリアクションのペアを指定できることがわかりました。 したがって、アプリケーションのモジュール全体をモックし、テストケースごとに、モックされたモジュールを異なる方法で反応させることができます。 さまざまな方法で、テストされたコンポーネントとモックされたコンポーネントのペアの可能な状態が反映されます。

ユニットテスト

このセクションでは、MVP(Model View Presenter)アーキテクチャを想定します。 アクティビティとフラグメントはビューであり、モデルはデータベースまたはリモートサービスへの呼び出しのリポジトリレイヤーであり、プレゼンターはこれらすべてをバインドする「頭脳」であり、ビュー、モデル、およびデータのフローを制御する特定のロジックを実装します。申し込み。

コンポーネントの抽象化

ビューとモデルをあざける

このAndroidテストの例では、ビュー、モデル、およびリポジトリコンポーネントをモックし、プレゼンターを単体テストします。 これは、アーキテクチャ内の単一のコンポーネントを対象とした最小のテストの1つです。 さらに、メソッドスタブを使用して、適切でテスト可能な一連の反応を設定します。

 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }

MockWebServerを使用したグローバルネットワーク層のモック

多くの場合、グローバルネットワーク層をモックできると便利です。 MockWebServerを使用すると、テストで実行する特定のリクエストに対する応答をキューに入れることができます。 これにより、サーバーに期待されるあいまいな応答をシミュレートする機会が得られますが、再現するのは簡単ではありません。 これにより、追加のコードをほとんど記述せずに、完全なカバレッジを確保できます。

MockWebServerのコードリポジトリは、このライブラリをよりよく理解するために参照できる優れた例を提供します。

カスタムテストダブル

Dagger(http://square.github.io/dagger/)を使用してオブジェクトグラフに別のモジュールを提供することにより、独自のモデルまたはrespoistoryコンポーネントを作成し、それをテストに注入できます。 モックモデルコンポーネントによって提供されたデータに基づいて、ビューステートが適切に更新されたかどうかを確認するオプションがあります。

 /** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }

テストの実行

Android Studio

テストクラス、メソッド、またはテストパッケージ全体を簡単に右クリックして、IDEのオプションダイアログからテストを実行できます。

ターミナル

ターミナルからAndroidアプリのテストを実行すると、ターゲットモジュールの「ビルド」フォルダーにテストされたクラスのレポートが作成されます。 さらに、自動ビルドプロセスをセットアップする場合は、ターミナルアプローチを使用します。 Gradleを使用すると、次のコマンドを実行することで、すべてのデバッグフレーバーテストを実行できます。

 gradle testDebug

AndroidStudioバージョンからソースセット「テスト」にアクセスする

AndroidStudioのバージョン1.1とAndroidGradleプラグインは、コードの単体テストをサポートします。 あなたはそれに関する彼らの優れたドキュメントを読むことによってより多くを学ぶことができます。 この機能は実験的なものですが、IDEから単体テストとインストルメンテーションテストのソースセットを簡単に切り替えることができるため、優れた機能も備えています。 IDEでフレーバーを切り替える場合と同じように動作します。

Androidユニットテスト

プロセスの簡素化

Androidアプリのテストを作成することは、元のアプリケーションを開発することほど楽しくない場合があります。 したがって、プロジェクトの設定中にテストを作成し、一般的な問題を回避するプロセスを容易にする方法に関するいくつかのヒントは、大いに役立ちます。

AssertJ Android

AssertJ Androidは、名前から推測できるように、Androidを念頭に置いて構築されたヘルパー関数のセットです。 これは、人気のあるライブラリAssertJの拡張です。 AssertJ Androidが提供する機能は、「assertThat(view).isGone()」などの単純なアサーションから、次のような複雑なものまでさまざまです。

 assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)

AssertJ Androidとその拡張性により、Androidアプリケーションのテストを作成するためのシンプルで優れた出発点が保証されます。

RobolectricおよびManifestPath

Robolectricを使用しているときに、マニフェストの場所を指定する必要があり、SDKのバージョンが18に設定されていることに気付く場合があります。これを行うには、「Config」アノテーションを含めます。

 @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)

ターミナルからRobolectricを必要とするテストを実行すると、新しい課題が発生する可能性があります。 たとえば、「テーマが設定されていません」などの例外が表示される場合があります。 テストがIDEから正しく実行されているが、ターミナルからは実行されていない場合は、指定されたマニフェストパスを解決できないターミナル内のパスからテストを実行しようとしている可能性があります。 マニフェストパスのハードコードされた構成値が、コマンドの実行時点から正しい場所を指していない可能性があります。 これは、カスタムランナーを使用することで解決できます。

 public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }

Gradle構成

以下を使用して、単体テスト用にGradleを構成できます。 プロジェクトのニーズに基づいて、必要な依存関係の名前とバージョンを変更する必要がある場合があります。

 // Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }

RobolectricおよびPlayサービス

Google Playサービスを使用している場合、このアプリケーション構成でRobolectricが正しく機能するには、Playサービスバージョン用に独自の整数定数を作成する必要があります。

 <meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />

ライブラリをサポートするためのロボレクトリック依存関係

もう1つの興味深いテストの問題は、Robolectricがサポートライブラリを適切に参照できないことです。 解決策は、テストが行​​われるモジュールに「project.properties」ファイルを追加することです。 たとえば、Support-v4およびAppCompatライブラリの場合、ファイルには次のものが含まれている必要があります。

 android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3

受け入れ/回帰テスト

受け入れ/回帰テストは、実際の100%パーセントのAndroid環境でのテストの最終ステップの一部を自動化します。 このレベルでは、モックされたAndroid OSクラスは使用しません。テストは、実際のデバイスとエミュレーターで実行されます。

アンドロイドの受け入れと回帰テスト

これらの状況では、物理デバイス、エミュレーター構成、デバイスの状態、および各デバイスの機能セットが多様であるため、プロセスがはるかに不安定になります。 さらに、コンテンツの表示方法を決定するのは、オペレーティングシステムのバージョンと電話の画面サイズに大きく依存します。

さまざまなデバイスに合格する適切なテストを作成するのは少し複雑ですが、いつものように、大きな夢を見て、小さなものから始める必要があります。 Robotiumを使用したテストの作成は、反復プロセスです。 いくつかのトリックで、それはかなり単純化することができます。

Robotium

Robotiumは、2010年1月から存在しているオープンソースのAndroidテスト自動化フレームワークです。Robotiumは有料ソリューションですが、公正な無料トライアルが付属しています。

Robotiumテストの作成プロセスをスピードアップするために、手動のテスト作成からテスト記録に移行します。 トレードオフは、コードの品質と速度の間です。 ユーザーインターフェイスに大幅な変更を加える場合は、テスト記録アプローチと新しいテストをすばやく記録できることで多くのメリットが得られます。

Testdroid Recorderは、ユーザーインターフェイスで実行したクリックを記録するときにRobotiumテストを作成する無料のテストレコーダーです。 ステップバイステップのビデオとともにドキュメントに記載されているように、ツールのインストールは非常に簡単です。

Testdroid RecorderはEclipseプラグインであり、この記事全体でAndroid Studioを参照しているため、理想的には懸念の理由になります。 ただし、この場合、プラグインをAPKで直接使用して、それに対するテストを記録できるため、問題はありません。

テストを作成したら、Testdroidレコーダが必要とする依存関係とともに、テス​​トをコピーしてAndroid Studioに貼り付けることができ、準備が整います。 記録されたテストは、以下のクラスのようになります。

 public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }

よく見ると、コードのどれだけがかなり単純であるかがわかります。

テストを記録するときは、「待機」ステートメントを不足させないでください。 ダイアログが表示され、アクティビティが表示され、テキストが表示されるのを待ちます。 これにより、現在の画面でアクションを実行するときに、アクティビティとビュー階層を操作できるようになります。 同時に、スクリーンショットを撮ります。 自動テストは通常​​無人であり、スクリーンショットはそれらのテスト中に実際に何が起こったかを確認する方法の1つです。

テストが成功するか失敗するかにかかわらず、レポートはあなたの親友です。 それらは、ビルドディレクトリ「module / build / outputs/reports」の下にあります。

テストレポート

理論的には、QAチームはテストを記録して最適化することができます。 テストケースを最適化するための標準化されたモデルに力を入れることによって、それを行うことができます。 通常、テストを記録するときは、問題なく機能するように、常にいくつかの調整を行う必要があります。

最後に、Android Studioからこれらのテストを実行するには、それらを選択して、単体テストを実行する場合と同じように実行できます。 ターミナルからは、ワンライナーです。

 gradle connectedAndroidTest

テストのパフォーマンス

Robolectricを使用したAndroidユニットテストは、マシンのJVM内で直接実行されるため、非常に高速です。 それと比較して、エミュレーターと物理デバイスでの受け入れテストははるかに遅くなります。 テストするフローのサイズに応じて、テストケースごとに数秒から数分かかる場合があります。 受け入れテストフェーズは、継続的インテグレーションサーバーでの自動ビルドプロセスの一部として使用する必要があります。

複数のデバイスで並列化することにより、速度を向上させることができます。 JakeWhartonとSquarehttp://square.github.io/spoon/のメンバーによるこのすばらしいツールをチェックしてください。 いくつかの素晴らしいレポートもあります。

テイクアウェイ

利用可能なAndroidテストツールにはさまざまなものがあり、エコシステムが成熟するにつれて、テスト可能な環境をセットアップしてテストを作成するプロセスが簡単になります。 取り組むべき課題はまだまだあり、日々の問題に取り組んでいる開発者の幅広いコミュニティでは、建設的な議論と迅速なフィードバックの余地がたくさんあります。

このAndroidテストチュートリアルで説明されているアプローチを使用して、前もって課題に取り組む際のガイドを提供します。 問題が発生した場合は、この記事またはリンクされているリファレンスで既知の問題の解決策を確認してください。

将来の投稿では、並列化、ビルドの自動化、継続的インテグレーション、Github / BitBucketフック、アーティファクトのバージョニング、および大規模なモバイルアプリケーションプロジェクトをより詳細に管理するためのベストプラクティスについて説明します。