テストは、高品質のソフトウェア製品を開発する上で最も重要な部分の1つです。 今日は、Androidアプリケーションのテストを作成するためにチームが開発および使用したいくつかの方法論とライブラリについてお話します。

より基本的なことから始めましょう。経験豊富な開発者がUIテスト用のツールのセクションに直接アクセスできるためです。 基本的なことを学び、リフレッシュしたい人のために-読書をお楽しみください。
最初のテストを作成する
テストする小さなコンポーネントを作成しましょう。 名前を含むJSONオブジェクトでファイルを解析し、結果の文字列を返します。
public class NameRepository { private final File file; public NameRepository(File file) { this.file = file; } public String getName() throws IOException { Gson gson = new Gson(); User user = gson.fromJson(readFile(), User.class); return user.name; } public String readFile() throws IOException { byte[] bytes = new byte[(int) file.length()]; try (FileInputStream in = new FileInputStream(file)) { in.read(bytes); } return new String(bytes, Charset.defaultCharset()); } private static final class User { String name; } }
ここと将来、コードの簡略版を提供します。 完全版はリポジトリで表示できます 。 完全なコードへのリンクが各スニペットに添付されます。
最初のJUnitテストを作成します。 JUnitは、テストを記述するためのJavaライブラリです。 JUnitがメソッドがテストであることを知るには、 @Test
Testアノテーションを追加する必要があります。 JUnitにはAssert
クラスが含まれています。これにより、実際の値と予想される値を比較し、値が一致しない場合はエラーを表示できます。 このテストは、コンポーネントの正確さ、つまりファイルの読み取り、JSONの解析、正しいフィールドの取得をテストします。
public class NameRepositoryTest { private static final File FILE = new File("test_file"); NameRepository nameRepository = new NameRepository(FILE); @Test public void getName_isSasha() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); FILE.delete(); } @Test public void getName_notMia() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); String name = nameRepository.getName(); Assert.assertNotEquals(name, "Mia"); FILE.delete(); } }
完全なコード
テストを書くためのライブラリ
テストもサポートが必要なコードです。 さらに、テストコードは頭の中で検証できるように理解しやすいものでなければなりません。 したがって、テストコードの単純化、重複の排除、読みやすさの向上に投資することは理にかなっています。 この問題に役立つ、広く使用されているライブラリを見てみましょう。
各テストで準備コードを複製しないために、 @Before
および@After
ます。 @Before
アノテーションでマークされたメソッドは各テストの前に実行され、 @Before
アノテーションでマークされたメソッドは各テストの後に実行されます。 アノテーション@BeforeClass
および@AfterClass
もあります。これらは、クラス内のすべてのテストの前後にそれぞれ実行されます。 次の方法を使用してテストをやり直しましょう。
public class NameRepositoryTest { private static final File FILE = new File("test_file"); NameRepository nameRepository = new NameRepository(FILE); @Before public void setUp() throws Exception { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter(new FileOutputStream(FILE), UTF_8)), true); writer.println("{name : Sasha}"); writer.close(); } @After public void tearDown() { FILE.delete(); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } @Test public void getName_notMia() throws Exception { String name = nameRepository.getName(); Assert.assertNotEquals(name, "Mia"); } }
完全なコード
各テストのセットアップコードの重複を削除することができました。 ただし、テストを含む多くの異なるクラスではファイルの作成が必要になる場合があり、この重複も削除することが望ましいでしょう。 これを行うために、テストルールのライブラリ( TestRule )があります。 テストルールは、 @Before
および@After
と同様の機能を実行します。 このクラスのapply()メソッドでは、各テストまたはすべてのテストの実行前後に必要なアクションを実行できます。 コードの重複を減らすことに加えて、このメソッドの利点は、テストクラスからコードが取り出されることです。これにより、テスト内のコードの量が減り、読みやすくなります。 ファイルを作成するためのルールを書きましょう:
public class CreateFileRule implements TestRule { private final File file; private final String text; public CreateFileRule(File file, String text) { this.file = file; this.text = text; } @Override public Statement apply(final Statement s, Description d) { return new Statement() { @Override public void evaluate() throws Throwable { PrintWriter writer = new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream(FILE), UTF_8)), true); writer.println(text); writer.close(); try { s.evaluate(); } finally { file.delete(); } } }; } }
完全なコード
テストではこのルールを使用します。 テストごとにTestRule
アクションを実行TestRule
アノテーションを付ける必要があります。
public class NameRepositoryTest { static final File FILE = new File("test_file"); @Rule public final CreateFileRule fileRule = new CreateFileRule(FILE, "{name : Sasha}"); NameRepository nameRepository = new NameRepository(new FileReader(FILE)); @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } ... }
完全なコード
ルールがアノテーション@ClassRule
でマークされている場合、アクションは各テストの前に呼び出されませんが、アノテーション@BeforeClass
および@AfterClass
と同様に、クラス内のすべてのテストの前に1回@AfterClass
ます。
複数のTestRule
をテストで使用する場合、特定の順序で開始する必要がある場合があります。これには、 TestRulesの起動順序を決定できるRuleChainがあります。 ファイルを作成する前にフォルダーを作成するルールを作成しましょう。
public class CreateDirRule implements TestRule { private final File dir; public CreateDirRule(File dir) { this.dir = dir; } @Override public Statement apply(final Statement s, Description d) { return new Statement() { @Override public void evaluate() throws Throwable { dir.mkdir(); try { s.evaluate(); } finally { dir.delete(); } } }; } }
完全なコード
このルールを使用すると、テストクラスは次のようになります。
public class NameRepositoryTest { static final File DIR = new File("test_dir"); static final File FILE = Paths.get(DIR.toString(), "test_file").toFile(); @Rule public final RuleChain chain = RuleChain .outerRule(new CreateDirRule(DIR)) .around(new CreateFileRule(FILE, "{name : Sasha}")); @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); Assert.assertEquals(name, "Sasha"); } ... }
完全なコード
これで、各テストで、ファイルを作成する前にディレクトリが作成され、ファイルを削除した後に削除されます。
Google Truthは、テストコードの可読性を向上させるためのライブラリです。 assertメソッド( JUnit Assertに似ています )が含まれていますが、より人間が読みやすく、パラメーターをチェックするためのオプションがはるかに多く含まれています。 これは、Truthを使用した以前のテストの外観です。
@Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } @Test public void getName_notMia() throws Exception { String name = nameRepository.getName(); assertThat(name).isNotEqualTo("Mia"); }
完全なコード
このコードは、話された英語のテキストのように読み取れることがわかります。
このコンポーネントは2つの異なる処理を実行します。ファイルを読み取り、解析します。 唯一の責任の原則に従うために、ファイルを読み取るロジックを個別のコンポーネントに分けましょう。
public class FileReader { private final File file; public FileReader(File file) { this.file = file; } public String readFile() throws IOException { byte[] bytes = new byte[(int) file.length()]; try (FileInputStream in = new FileInputStream(file)) { in.read(bytes); } return new String(bytes, Charset.defaultCharset()); } }
完全なコード
ここでNameRepository
をテストしNameRepository
が、実際にはFileReader
ファイルの読み取りもテストしています。 これを回避し、テストの分離、信頼性、速度を向上させるために、実際のFileReader
をそのモックに置き換えることができます。
Mockitoは、テストで使用する実際のオブジェクトの代わりにスタブ(モック)を作成するためのライブラリです。 Mockitoを使用して実行できるいくつかのアクション:
クラスとインターフェイスのスタブを作成する
このメソッドに渡されたメソッド呼び出しと値を確認してください。
メソッド呼び出しを制御するための実際のスパイスパイオブジェクトへの接続。
FileReader
モックを作成し、 readFile()
メソッドが必要な文字列を返すように構成します。
public class NameRepositoryTest { FileReader fileReader = mock(FileReader.class); NameRepository nameRepository = new NameRepository(fileReader); @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } }
完全なコード
これで、ファイルの読み取りは発生しません。 代わりに、モックはテストで構成された値を提供します。
mokの使用には次の利点があります。
- テストはテストされたクラスのエラーのみをチェックし、他のクラスのエラーはテストされたクラスのテストにいかなる影響も与えません
- 時には短くて読みやすいコード
- メソッド呼び出しをチェックし、凍結オブジェクトのメソッドに値を渡すことができます
と欠点:
- デフォルトでは、使用されているすべてのメソッドを明示的に構成する必要があるため、未構成のメソッドはnullを返します。
- 実際のオブジェクトに状態がある場合、変更するたびにそのモックを再構成する必要があります。そのため、テストコードが肥大化することがあります。
mokを作成するより簡単で便利な方法があります-特別な@Mock
アノテーションを使用します:
@Mock File file;
そのようなmokaを初期化するには、3つの方法があります。
@Before public void setUp() { MockitoAnnotations.initMocks(this); }
@RunWith(MockitoJUnitRunner.class)
@Rule public final MockitoRule rule = MockitoJUnit.rule();
2番目のオプションは最も宣言的でコンパクトですが、特別なテストランナーを使用する必要がありますが、これは必ずしも便利ではありません。 後者のオプションにはこの欠点がなく、 initMocks()
メソッドを使用するよりも宣言的です。
MockitoJUnitRunnerの例 @RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { @Mock FileReader fileReader; NameRepository nameRepository; @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); nameRepository = new NameRepository(fileReader); } @Test public void getName_isSasha() throws Exception { String name = nameRepository.getName(); assertThat(name).isEqualTo("Sasha"); } }
完全なコード
ホストJava VMとAndroid Java VM
Androidテストは、通常のJava VMで実行できるテストと、Android Java VMで実行する必要があるテストの2種類に分類できます。 両方のタイプのテストを見てみましょう。
テストは通常のJava VMで実行されます
Androidエミュレーターまたは実際のデバイスを必要とするAndroid APIコンポーネントの操作を必要としないコードのテストは、コンピューターおよび任意のJavaマシンで直接実行できます。 ほとんどの場合、これらは単一のクラスを分離してテストするビジネスロジックユニットテストです。 統合テストは、テストされるクラスが相互作用する実際のクラスオブジェクトを作成することは常に可能とはほど遠いため、あまり頻繁に作成されません。
ホストJavaテストでクラスを作成するには、javaファイルにパス${moduleName}/src/test/java/...
また、 @RunWith
アノテーションを使用して、テストを実行し、すべてのメソッドを正しく呼び出して処理する責任があるRunner
を指定します。
@RunWith(MockitoJUnitRunner.class) public class TestClass {...}
これらのテストを使用すると、多くの利点があります。
- エミュレータまたは実際のデバイスを実行する必要はありません。これは、エミュレータが非常に遅く動作し、実際のデバイスが存在しない継続的インテグレーションのテストに合格する場合に特に重要です。
- アプリケーションを実行したり、UIを表示したりする必要がないため、非常に迅速にパスします。
- エミュレーターがフリーズするなどの事実に関連する問題がないため、安定しています。
一方、これらのテストでは:
- クラスとオペレーティングシステムの相互作用を完全にテストすることはできません。
- 特に、UI要素とジェスチャーのクリックをテストすることはできません
Host JavaテストでAndroid APIクラスを使用できるようにするために、Android環境をエミュレートし、その主要機能へのアクセスを提供するRobolectricライブラリがあります。 ただし、Roboelectricを使用したAndroidクラスのテストは不安定になることがよくあります。Robolectricが最新のAndroid APIをサポートするまでに時間がかかり、リソースの取得などに問題があります。 したがって、実際のクラスはほとんど使用されず、それらのmokiは単体テストに使用されます。
Roboelectricを使用してテストを実行するには、カスタムTestRunnerをインストールする必要があります。 その中で、SDKバージョン(最新の安定バージョンは23)を構成し、エミュレートされたAndroid環境のメインApplication
クラスおよびその他のパラメーターを指定できます。
public class MainApplication extends Application {}
完全なコード
@RunWith(RobolectricTestRunner.class) @Config(sdk = 21, application = MainApplication.class) public class MainApplicationTest { @Test public void packageName() { assertThat(RuntimeEnvironment.application) .isInstanceOf(MainApplication.class); } }
完全なコード
Android Java VMで実行されるテスト
機器のテストでは、ボタンの押下、テキスト入力、その他のアクションをテストするため、デバイスまたはエミュレーターの存在が必須です。
Android Java VMのテストを作成するには、パス${moduleName}/src/androidTest/java/...
に沿ってjavaファイルを配置し、Androidデバイスでテストを実行できるようにする@RunWith
アノテーションを使用してAndroidJUnit4
を指定する必要があります。
@RunWith(AndroidJUnit4.class) public class TestClass {...}
UIテスト
UIをテストするには、プログラムのユーザーインターフェイスをテストするためのAPIを提供するEspressoフレームワークが使用されます。 Espressoでは、テストはバックグラウンドストリームで実行され、UIストリームのUI要素と対話します。 エスプレッソには、テスト用のいくつかの主要なクラスがあります。
- エスプレッソはメインクラスです。 システムボタン(戻る、ホーム)を押す、キーボードを呼び出す/非表示にする、メニューを開く、コンポーネントにアクセスするなどの静的メソッドが含まれています。
- ViewMatchers-現在の階層の画面でコンポーネントを見つけることができます。
- ViewActions-コンポーネントと対話できます(クリック、ロングクリック、ダブルクリック、スワイプ、スクロールなど)。
- ViewAssertions-コンポーネントのステータスを確認できます。
最初のUIテスト
テストする最も単純なAndroidアプリケーションを作成します。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); } }
完全なコード
アプリケーションをテストします。 UIをテストするときは、最初にアクティビティを開始する必要があります。 これを行うために、各テストの前にアクティビティを開始し、後に終了するActivityTestRuleがあります。
@Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
id R.id.container
の要素が画面に表示されることを確認する簡単なテストを作成します。
@RunWith(AndroidJUnit4.class) public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class); @Test public void checkContainerIsDisplayed() { onView(ViewMatchers.withId(R.id.container)) .check(matches(isDisplayed())); } }
完全なコード
ロックを解除して画面をオンにする
低速またはビジーなマシンのエミュレーターは、ゆっくり実行できます。 したがって、エミュレーターの開始からエミュレーターへのアプリケーションのインストールを伴うビルドの終了までの間に、画面が非アクティブにならないように十分な時間が経過する場合があります。 したがって、ロックされた画面でテストを実行すると、 java.lang.RuntimeException: Could not launch activity within 45 seconds
エラーが発生します。 したがって、アクティビティを開始する前に、ロックを解除して画面をオンにする必要があります。 これは各UIテストで行う必要があるため、コードの重複を避けるために、テストの前にロックを解除して画面をオンにするルールを作成します。
class UnlockScreenRule<A extends AppCompatActivity> implements TestRule { ActivityTestRule<A> activityRule; UnlockScreenRule(ActivityTestRule<A> activityRule) { this.activityRule = activityRule; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { activityRule.runOnUiThread(() -> activityRule .getActivity() .getWindow() .addFlags( WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)); statement.evaluate(); } }; } }
完全なコード
エミュレーター画面のロックを解除し、テストを実行する前にアクティビティを起動するカスタムActivityTestRule
を作成します。
ActivityTestRule public class ActivityTestRule<A extends AppCompatActivity> implements TestRule { private final android.support.test.rule.ActivityTestRule<A> activityRule; private final RuleChain ruleChain; public ActivityTestRule(Class<A> activityClass) { this.activityRule = new ActivityTestRule<>(activityClass, true, true); ruleChain = RuleChain .outerRule(activityRule) .around(new UnlockScreenRule(activityRule)); } public android.support.test.rule.ActivityTestRule<A> getActivityRule() { return activityRule; } public void runOnUiThread(Runnable runnable) throws Throwable { activityRule.runOnUiThread(runnable); } public A getActivity() { return activityRule.getActivity(); } @Override public Statement apply(Statement statement, Description description) { return ruleChain.apply(statement, description); } }
完全なコード
標準のルールの代わりにこのルールを使用すると、CIのUIテストのランダムクラッシュの数を大幅に減らすことができます。
フラグメントテスト
通常、アプリケーションUIのレイアウトとロジックはすべてアクティビティに入れられるのではなく、ウィンドウに分割され、それぞれにフラグメントが作成されます。 NameRepository
を使用して名前を表示する簡単なスニペットを作成しましょう。
public class UserFragment extends Fragment { private TextView textView; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); try { textView.setText(createNameRepository().getName()); } catch (IOException exception) { throw new RuntimeException(exception); } return textView; } private NameRepository createNameRepository() { return new NameRepository( new FileReader( new File( getContext().getFilesDir().getAbsoluteFile() + File.separator + "test_file"))); } @Override public void onDestroyView() { super.onDestroyView(); textView = null; } }
完全なコード
フラグメントを開くと、UIがしばらくフリーズする場合があり、フラグメント間の遷移のアニメーションが使用される場合、フラグメントが表示される前にテストを開始できます。 したがって、フラグメントを開くだけでなく、起動するまで待つ必要があります。 非常にシンプルで明確な構文を持つAwaitilityライブラリは、アクションの実行結果を待つのに最適です。 フラグメントを開始し、このライブラリを使用して起動されることを期待するルールを作成します。
class OpenFragmentRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final Fragment fragment; OpenFragmentRule(ActivityTestRule<A> activityRule, Fragment fragment) { this.activityRule = activityRule; this.fragment = fragment; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { openFragment(fragment); await().atMost(5, SECONDS).until(fragment::isResumed); statement.evaluate(); } }; } }
完全なコード
この場合、式は、フラグメントが5秒以内に開始しない場合、テストに合格しないことを意味します。 フラグメントが開始されるとすぐに、テストはすぐに実行を継続し、5秒すべて待機しないことに注意してください。
アクティビティを起動するルールと同様に、フラグメントを起動するルールを作成することは論理的です。
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment> implements TestRule { private ActivityTestRule<A> activityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule(Class<A> activityClass, F fragment) { this.fragment = fragment; this.activityRule = new ActivityTestRule<>(activityClass); ruleChain = RuleChain .outerRule(activityRule) .around(new OpenFragmentRule<>(activityRule, fragment)); } public ActivityTestRule<A> getActivityRule() { return activityRule; } public F getFragment() { return fragment; } public void runOnUiThread(Runnable runnable) throws Throwable { activityRule.runOnUiThread(runnable); } public A getActivity() { return activityRule.getActivity(); } @Override public Statement apply(Statement statement, Description description) { return ruleChain.apply(statement, description); } }
完全なコード
このルールを使用したフラグメントテストは次のようになります。
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @Rule public final RuleChain rules = RuleChain .outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}")) .around(new FragmentTestRule<>(MainActivity.class, new UserFragment())); @Test public void nameDisplayed() { onView(withText("Sasha")).check(matches(isDisplayed())); } private File getTestFile() { return new File( InstrumentationRegistry.getTargetContext() .getFilesDir() .getAbsoluteFile() + File.separator + "test_file"); } }
完全なコード
フラグメントでの非同期データの読み込み
ディスクでの操作、つまりファイルからの名前の取得には比較的時間がかかるため、この操作は非同期で実行する必要があります。 ファイルから名前を非同期的に取得するには、 RxJavaライブラリを使用します。 RxJavaは現在、ほとんどのAndroidアプリケーションで使用されていると自信を持って言えます。 非同期で実行する必要があるほとんどすべてのタスクは、RxJavaを使用して実行されます。これは、おそらく非同期コード実行のための最も便利で理解しやすいライブラリの1つだからです。
非同期で動作するようにリポジトリを変更します。
public class NameRepository { ... public Single<String> getName() { return Single.create( emitter -> { Gson gson = new Gson(); emitter.onSuccess( gson.fromJson(fileReader.readFile(), User.class).getName()); }); } }
完全なコード
RXコードをテストするために、特別なクラスTestObserver
があります。これは、 Observable
自動的にサブスクライブし、即座に結果を受け取ります。 リポジトリテストは次のようになります。
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { ... @Test public void getName() { TestObserver<String> observer = nameRepository.getName().test(); observer.assertValue("Sasha"); } }
完全なコード
新しいリアクティブリポジトリを使用してフラグメントを更新します。
public class UserFragment extends Fragment { ... @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); createNameRepository() .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } }
, Awaitility:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { ... @Test public void nameDisplayed() { await() .atMost(5, SECONDS) .ignoreExceptions() .untilAsserted( () -> onView(ViewMatchers.withText("Sasha")) .check(matches(isDisplayed()))); } }
, — , , , . , , textView
null
. NullPointerException
textView
subscribe()
, :
public class UserFragment extends Fragment { private TextView textView; private Disposable disposable; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { textView = new TextView(getActivity()); disposable = createNameRepository() .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } @Override public void onDestroyView() { super.onDestroyView(); disposable.dispose(); textView = null; } }
, , . . onCreateView
textView
null
, . :
public class FragmentAsyncTestRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final Fragment fragment; public FragmentAsyncTestRule(Class<A> activityClass, Fragment fragment) { this.activityRule = new ActivityTestRule<>(activityClass); this.fragment = fragment; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } finally { activityRule.launchActivity(new Intent()); openFragment(fragment); openFragment(new Fragment()); } } }; } }
:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); ... }
, .
- Rx
, Observable
, timeout
:
public class UserPresenter { public interface Listener { void onUserNameLoaded(String name); void onGettingUserNameError(String message); } private final Listener listener; private final NameRepository nameRepository; public UserPresenter(Listener listener, NameRepository nameRepository) { this.listener = listener; this.nameRepository = nameRepository; } public void getUserName() { nameRepository .getName() .timeout(2, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( listener::onUserNameLoaded, error -> listener.onGettingUserNameError(error.getMessage())); } }
, . :
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(Observable.just("Sasha")); presenter = new UserPresenter(listener, nameRepository); } @Test public void getUserName() { presenter.getUserName(); verifyNoMoreInteractions(listener); } }
listener
, , . Awaitility . - , RxJava Schedulers
. TestScheduler , , Observable
, . , :
RxImmediateSchedulerRule public class RxImmediateSchedulerRule implements TestRule { private static final TestScheduler TEST_SCHEDULER = new TestScheduler(); private static final Scheduler IMMEDIATE_SCHEDULER = new Scheduler() { @Override public Disposable scheduleDirect(Runnable run, long delay, TimeUnit unit) { return super.scheduleDirect(run, 0, unit); } @Override public Worker createWorker() { return new ExecutorScheduler.ExecutorWorker(Runnable::run); } }; @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { RxJavaPlugins.setIoSchedulerHandler(scheduler -> TEST_SCHEDULER); RxJavaPlugins.setComputationSchedulerHandler( scheduler -> TEST_SCHEDULER); RxJavaPlugins.setNewThreadSchedulerHandler( scheduler -> TEST_SCHEDULER); RxAndroidPlugins.setMainThreadSchedulerHandler( scheduler -> IMMEDIATE_SCHEDULER); try { base.evaluate(); } finally { RxJavaPlugins.reset(); RxAndroidPlugins.reset(); } } }; } public TestScheduler getTestScheduler() { return TEST_SCHEDULER; } }
完全なコード
:
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { static final int TIMEOUT_SEC = 2; static final String NAME = "Sasha"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository); } @Test public void getUserName() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onUserNameLoaded(NAME); } @Test public void getUserName_timeout() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onGettingUserNameError(any()); } }
, Dagger 2
Dependency Injection . Dagger 2 — , . Android Dagger. , , , .
, Dagger ApplicationComponent
, , Application
, , , .
ApplicationComponent @Singleton @Component(modules = {ContextModule.class}) public interface ApplicationComponent { UserComponent createUserComponent(); }
完全なコード
MainApplication public class MainApplication extends Application { private ApplicationComponent component; @Override public void onCreate() { super.onCreate(); component = DaggerApplicationComponent.builder() .contextModule(new ContextModule(this)) .build(); } public ApplicationComponent getComponent() { return component; } }
完全なコード
Dagger , :
UserModule @Module public class UserModule { @Provides NameRepository provideNameRepository(@Private FileReader fileReader) { return new NameRepository(fileReader); } @Private @Provides FileReader provideFileReader(@Private File file) { return new FileReader(file); } @Private @Provides File provideFile(Context context) { return new File(context.getFilesDir().getAbsoluteFile() + File.separator + "test_file"); } @Qualifier @Retention(RetentionPolicy.RUNTIME) private @interface Private {} }
完全なコード
, Dagger:
public class UserFragment extends Fragment { ... @Inject NameRepository nameRepository; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((MainApplication) getActivity().getApplication()) .getComponent() .createUserComponent() .injectsUserFragment(this); textView = new TextView(getActivity()); disposable = nameRepository .getName() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(name -> textView.setText(name)); return textView; } }
UI unit- . Dagger, ApplicationComponent
. Application
:
public void setComponentForTest(ApplicationComponent component) { this.component = component; }
, :
class TestDaggerComponentRule<A extends AppCompatActivity> implements TestRule { private final ActivityTestRule<A> activityRule; private final ApplicationComponent component; TestDaggerComponentRule( ActivityTestRule<A> activityRule, ApplicationComponent component) { this.activityRule = activityRule; this.component = component; } @Override public Statement apply(Statement statement, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { MainApplication application = ((MainApplication) activityRule.getActivity().getApplication()); ApplicationComponent originalComponent = application.getComponent(); application.setComponentForTest(component); try { statement.evaluate(); } finally { application.setComponentForTest(originalComponent); } } }; } }
, , Application . , . , , Dagger , .
public class FragmentTestRule<A extends AppCompatActivity, F extends Fragment> implements TestRule { private ActivityTestRule<A> activityRule; private F fragment; private RuleChain ruleChain; public FragmentTestRule( Class<A> activityClass, F fragment, ApplicationComponent component) { this.fragment = fragment; this.activityRule = new ActivityTestRule<>(activityClass); ruleChain = RuleChain .outerRule(activityRule) .around(new TestDaggerComponentRule<>(activityRule, component)) .around(new OpenFragmentRule<>(activityRule, fragment)); } ... }
:
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { ... @Rule public final FragmentTestRule<MainActivity, UserFragment> fragmentRule = new FragmentTestRule<>( MainActivity.class, new UserFragment(), createTestApplicationComponent()); private ApplicationComponent createTestApplicationComponent() { ApplicationComponent component = mock(ApplicationComponent.class); when(component.createUserComponent()) .thenReturn(DaggerUserFragmentTest_TestUserComponent.create()); return component; } @Singleton @Component(modules = {TestUserModule.class}) interface TestUserComponent extends UserComponent {} @Module static class TestUserModule { @Provides public NameRepository provideNameRepository() { NameRepository nameRepository = mock(NameRepository.class); when(nameRepository.getName()).thenReturn( Single.fromCallable(() -> "Sasha")); return nameRepository; } } }
Debug
, UI, debug. , debug , :
class UserPresenter { ... public void getUserName() { nameRepository .getName() .timeout(TIMEOUT_SEC, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( name -> { listener.onUserNameLoaded(name); if (BuildConfig.DEBUG) { logger.info(String.format("Name loaded: %s", name)); } }, error -> listener.onGettingUserNameError(error.getMessage())); } }
, . DebugTestRule
, :
public class DebugRule implements TestRule { @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { if (BuildConfig.DEBUG) { base.evaluate(); } } }; } }
:
class UserPresenterDebugTest { ... @Rule public final DebugTestsRule debugRule = new DebugTestsRule(); @Test public void userNameLogged() { presenter.getUserName(); timeoutRule.getTestScheduler().triggerActions(); nameObservable.onNext(NAME); verify(logger).info(contains(NAME)); } }
おわりに
, TestRule , , , . , .
, .
public class NameRepository { private final FileReader fileReader; public NameRepository(FileReader fileReader) { this.fileReader = fileReader; } public Single<String> getName() { return Single.create( emitter -> { Gson gson = new Gson(); emitter.onSuccess( gson.fromJson(fileReader.readFile(), User.class).name); }); } private static final class User { String name; } }
@RunWith(MockitoJUnitRunner.class) public class NameRepositoryTest { @Mock FileReader fileReader; NameRepository nameRepository; @Before public void setUp() throws IOException { when(fileReader.readFile()).thenReturn("{name : Sasha}"); nameRepository = new NameRepository(fileReader); } @Test public void getName() { TestObserver<String> observer = nameRepository.getName().test(); observer.assertValue("Sasha"); } }
public class UserPresenter { public interface Listener { void onUserNameLoaded(String name); void onGettingUserNameError(String message); } private final Listener listener; private final NameRepository nameRepository; private final Logger logger; private Disposable disposable; public UserPresenter( Listener listener, NameRepository nameRepository, Logger logger) { this.listener = listener; this.nameRepository = nameRepository; this.logger = logger; } public void getUserName() { disposable = nameRepository .getName() .timeout(2, SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( name -> { listener.onUserNameLoaded(name); if (BuildConfig.DEBUG) { logger.info(String.format("Name loaded: %s", name)); } }, error -> listener.onGettingUserNameError(error.getMessage())); } public void stopLoading() { disposable.dispose(); } }
@RunWith(RobolectricTestRunner.class) public class UserPresenterTest { static final int TIMEOUT_SEC = 2; static final String NAME = "Sasha"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; @Mock Logger logger; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository, logger); } @Test public void getUserName() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC - 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onUserNameLoaded(NAME); } @Test public void getUserName_timeout() { presenter.getUserName(); timeoutRule.getTestScheduler().advanceTimeBy(TIMEOUT_SEC + 1, SECONDS); nameObservable.onNext(NAME); verify(listener).onGettingUserNameError(any()); } }
@RunWith(RobolectricTestRunner.class) public class UserPresenterDebugTest { private static final String NAME = "Sasha"; @Rule public final DebugRule debugRule = new DebugRule(); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule public final RxImmediateSchedulerRule timeoutRule = new RxImmediateSchedulerRule(); @Mock UserPresenter.Listener listener; @Mock NameRepository nameRepository; @Mock Logger logger; PublishSubject<String> nameObservable = PublishSubject.create(); UserPresenter presenter; @Before public void setUp() { when(nameRepository.getName()).thenReturn(nameObservable.firstOrError()); presenter = new UserPresenter(listener, nameRepository, logger); } @Test public void userNameLogged() { presenter.getUserName(); timeoutRule.getTestScheduler().triggerActions(); nameObservable.onNext(NAME); verify(logger).info(contains(NAME)); } }
public class UserFragment extends Fragment implements UserPresenter.Listener { private TextView textView; @Inject UserPresenter userPresenter; @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ((MainApplication) getActivity().getApplication()) .getComponent() .createUserComponent(new UserModule(this)) .injectsUserFragment(this); textView = new TextView(getActivity()); userPresenter.getUserName(); return textView; } @Override public void onUserNameLoaded(String name) { textView.setText(name); } @Override public void onGettingUserNameError(String message) { textView.setText(message); } @Override public void onDestroyView() { super.onDestroyView(); userPresenter.stopLoading(); textView = null; } }
@RunWith(AndroidJUnit4.class) public class UserFragmentIntegrationTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); @Rule public final RuleChain rules = RuleChain .outerRule(new CreateFileRule(getTestFile(), "{name : Sasha}")) .around(new FragmentTestRule<>(MainActivity.class, new UserFragment())); @Test public void nameDisplayed() { await() .atMost(5, SECONDS) .ignoreExceptions() .untilAsserted( () -> onView(ViewMatchers.withText("Sasha")) .check(matches(isDisplayed()))); } private static File getTestFile() { return new File( InstrumentationRegistry.getTargetContext() .getFilesDir() .getAbsoluteFile() + File.separator + "test_file"); } }
@RunWith(AndroidJUnit4.class) public class UserFragmentTest { @ClassRule public static TestRule asyncRule = new FragmentAsyncTestRule<>(MainActivity.class, new UserFragment()); @Rule public final FragmentTestRule<MainActivity, UserFragment> fragmentRule = new FragmentTestRule<>( MainActivity.class, new UserFragment(), createTestApplicationComponent()); @Test public void getNameMethodCalledOnCreate() { verify(fragmentRule.getFragment().userPresenter).getUserName(); } private ApplicationComponent createTestApplicationComponent() { ApplicationComponent component = mock(ApplicationComponent.class); when(component.createUserComponent(any(UserModule.class))) .thenReturn(DaggerUserFragmentTest_TestUserComponent.create()); return component; } @Singleton @Component(modules = {TestUserModule.class}) interface TestUserComponent extends UserComponent {} @Module static class TestUserModule { @Provides public UserPresenter provideUserPresenter() { return mock(UserPresenter.class); } } }
謝辞
Evgeny Aseev . . — Andrei Tarashkevich , Ruslan Login . , AURA Devices.