Unit test in android

1,225 views
1,015 views

Published on

Unit test in Android using Robolectric.

Published in: Technology
0 Comments
4 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,225
On SlideShare
0
From Embeds
0
Number of Embeds
44
Actions
Shares
0
Downloads
3
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

Unit test in android

  1. 1. ユニットテスト入門 Androidアプリケーションへのユニットテスト導入
  2. 2. Tatsuya Maki Android Application Developer
  3. 3. はなすこと 1. ユニットテストの導入 2. 効率的なユニットテスト 3. ユニットテストのTips
  4. 4. ユニットテストの導入 1
  5. 5. Q. ユニットテストとは
  6. 6. A. メソッドなど小さな単位で行うテスト
  7. 7. public static String fizzbuzz(int value) { String message; if (value % 15 == 0) { message = "FizzBuzz"; } else if (value % 5 == 0) { message = "Buzz"; } else if (value % 3 == 0) { message = "Fizz"; } else { message = String.valueOf(value); } return message; } FizzBuzz
  8. 8. @Test public void fizzbuzzShouldReturnBuzzWhenValueIs10() { // 実行結果 String actualValue = fizzbuzz(10); ! // 期待結果 String expectedValue = "Buzz"; ! // テスト assertThat(actualValue, is(expectedValue)); } FizzBuzzTest
  9. 9. Androidでの問題点 1. デバイスが必須 2. 実行速度が遅い 3. テストしにくい
  10. 10. Robolectric http://robolectric.org/
  11. 11. 遅い DalvikVM 必要 JUnit3 速い JVM 不要 JUnit4 AndroidTestCase 実行速度 VM デバイス JUnit Robolectric
  12. 12. TextView Shadow getText() Hello, world.
  13. 13. @Implements(TextView.class) class MyShadowTextView extends ShadowView { ! @Implementation public CharSequence getText() { return "Hello, Robolectric!"; } ! } Shadowの例
  14. 14. @Test @Config(shadows = { MyShadowTextView.class }) public void getTextShouldReturnHelloRobolectric() { // SetUp Context context = Robolectric.application.getApplicationContext(); TextView textView = new TextView(context); // Exercise CharSequence actualText = textView.getText(); // Verify assertEquals(actualText, "Hello, Robolectric!"); } Shadowの利用例
  15. 15. まとめ 1. デバイスが不要 2. JVM上で動作する 3. Shadowオブジェクト
  16. 16. 効率的なユニットテスト 2
  17. 17. 3つのツール 1. FEST 2. Mockito 3. EclEmma
  18. 18. FEST https://code.google.com/p/fest/
  19. 19. Q.アサーションとは
  20. 20. A. 実行結果と期待結果を比較検証する宣言
  21. 21. // 実行結果 int actualValue = 10 / 2; ! // 期待結果 int expectedValue = 5; ! // "10 / 2"が"5"と等しくなることを表明 assertEquals(actualValue, expectedValue); アサーションの例
  22. 22. FESTのメリット 1. アサーションの記述が容易 2. エラーメッセージが明確 3. Androidと親和性が高い
  23. 23. アサーションの記述が容易
  24. 24. // Exercise List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); ! ! // Verify assertNotNull(devices); assertEquals(2, devices.size()); assertTrue(devices.contains("Nexus 5")); JUnit
  25. 25. // Exercise List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); ! ! // Verify assertThat(devices, is(notNullValue())); assertThat(devices.size(), is(equalTo(2))); assertThat(devices, hasItem("Nexus 5")); Hamcrest
  26. 26. // Exercise List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); ! ! // Verify assertThat(devices) .isNotNull() .hasSize(2) .contains("Nexus 5"); FEST
  27. 27. エラーメッセージが明確
  28. 28. // テストコード List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); assertTrue(devices.contains("Nexus 4")); ! ! // エラーメッセージ java.lang.AssertionError ! ! ! ! JUnit
  29. 29. // テストコード List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); assertThat(devices, hasItem("Nexus 4")); ! ! // エラーメッセージ java.lang.AssertionError: Expected: a collection containing "Nexus 4" but: was "Nexus 5", was "Nexus 7” ! ! Hamcrest
  30. 30. // テストコード List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); assertThat(devices).contains("Nexus 4"); ! ! // エラーメッセージ java.lang.AssertionError: expecting: <['Nexus 5', 'Nexus 7']> to contain: <['Neuxus 4']> but could not find: <['Neuxus 4']> FEST
  31. 31. Androidと親和性が高い
  32. 32. // テストコード TextView textView = (TextView) activity .findViewById(R.id.text_view); assertThat(textView.getText()) .isEqualTo(activity.getString(R.string.message)); assertThat(textView.getVisibility()) .isEqualTo(View.VISIBLE); ! ! // エラーメッセージ org.junit.ComparisonFailure: expected: <[0]> but was: <[8]> FEST
  33. 33. // テストコード TextView textView = (TextView) activity .findViewById(R.id.text_view); assertThat(textView) .hasText(R.string.message) .isVisible(); ! ! ! // エラーメッセージ java.lang.AssertionError: Expected to be visible but was gone ! FEST Android
  34. 34. Mockito http://mockito.org/
  35. 35. Q. モックオブジェクトとは
  36. 36. A. オブジェクトの呼び出しを検証する
  37. 37. class MockInputStream extends InputStream { ! private boolean mIsRead; private boolean mIsClosed; ! @Override public int read() throws IOException { mIsRead = true; return 0; } ! public boolean isRead() { return mIsRead; } ! @Override public void close() throws IOException { mIsClosed = true; } ! public boolean isClosed() { return mIsClosed; } ! } モックオブジェクトの例
  38. 38. Mockitoでできること 1. 呼び出しの検証 2. 振る舞いの変更 3. フィールドの変更
  39. 39. // モックオブジェクトの生成 InputStream mocked = mock(InputStream.class); // InputStreamをクローズする mocked.close(); ! // closeメソッドの呼び出し検証 verify(mocked).close(); 呼び出しの検証
  40. 40. 振る舞いの変更 // モックオブジェクトの作成 InputStream mocked = mock(InputStream.class); ! // 戻り値の変更 when(mocked.read()).thenReturn(-1); ! // 例外の送出 when(mocked.read()).thenThrow(new IOException()); ! // ロジックの変更 when(mocked.read()).thenAnswer(new Answer<Integer>() { ! @Override public Integer answer(InvocationOnMock invocation) throws Throwable { int length; ... return length; } });
  41. 41. フィールドの変更 // 内部にInputStreamを保持するクラス MyObject object = new MyObject(); // フィールドの取得 InputStream stream = (InputStream) Whitebox .getInternalState(object, “mStream"); ! // スパイオブジェクトの作成 Socket spied = spy(stream); ! // フィールドの変更 Whitebox .setInternalState(object, "mStream", spied);
  42. 42. Mockitoでできないこと 1. finalクラス/メソッドのモック 2. privateメソッドのモック 3. staticメソッドのモック
  43. 43. EclEmma http://www.eclemma.org/
  44. 44. Q. カバレッジとは
  45. 45. A. テストがどれだけ網羅できているか
  46. 46. カバレッジの種類 C0: 命令網羅率 C1: 分岐網羅率 C2: 条件網羅率
  47. 47. EclEmmaで測定できるもの C0: 命令網羅率 C1: 分岐網羅率 C2: 条件網羅率
  48. 48. EclEmmaで測定できないもの C0: 命令網羅率 C1: 分岐網羅率 C2: 条件網羅率
  49. 49. ユニットテストのTips 3
  50. 50. Q. void型メソッドをテストしたい
  51. 51. A. 別のメソッドを使って検証する
  52. 52. List<String> list = new MyList<String>(); list.add("Android"); // 別のメソッドで検証 assertThat(list).hasSize(1); サンプル
  53. 53. A. メソッドの呼び出しを検証する
  54. 54. InputStream mocked = mock(InputStream.class); IoUtils.close(mocked); ! // closeメソッドの呼び出し検証 verify(mocked).close(); サンプル
  55. 55. Q. privateメソッドをテストしたい
  56. 56. A. 諦める
  57. 57. A. package privateに変更する
  58. 58. // privateメソッドなので呼び出せない private String buildMessage() { String message; // do something return message; } ! // package privateに変更 String buildMessage() { String message; // do something return message; } サンプル
  59. 59. Q. 非同期処理をテストしたい
  60. 60. A. CountDownLatchを使う
  61. 61. // timeoutを指定 @Test(timeout = 1000) public void test() throws InterruptedException{ CountDownLatch latch = new CountDownLatch(1); AsyncProcess.execute(new Callback() { @Override public void onComplete() { // 処理完了時にcountDownを呼出 latch.countDown(); } }); ! // 処理完了まで待機 latch.await(); ! ... } サンプル
  62. 62. Q. HTTP通信の外部依存をなくしたい
  63. 63. A. FakeHttpLayerを使う HttpClientの場合
  64. 64. // 常に同じレスポンスを返却 FakeHttpLayer layer = Robolectric.getFakeHttpLayer(); layer.setDefaultHttpResponse(200, "Hello, World!"); ! ! // 順番にレスポンスを変更 FakeHttpLayer layer = Robolectric.getFakeHttpLayer(); layer.addPendingHttpResponse(200, "Hello, Nexus 4!"); layer.addPendingHttpResponse(200, "Hello, Nexus 5!"); layer.addPendingHttpResponse(200, "Hello, Nexus 7!"); ! ! // 動的にレスポンスを変更 FakeHttpLayer layer = Robolectric.getFakeHttpLayer(); layer.addHttpResponseRule(new MyResponseRule()); サンプル
  65. 65. A. URLStreamHandlerを設定する HttpURLConnectionの場合
  66. 66. class StubURLStreamHandler extends URLStreamHandler { @Override protected URLConnection openConnection(URL url) throws IOException { // HttpURLConnectionを作成 return new StuHttpURLConnection(url); } } ! class StubURLStreamHandlerFactory implements URLStreamHandlerFactory { @Override public URLStreamHandler createURLStreamHandler( String protocol) { // URLStreamHandlerを作成 return new StubURLStreamHandler(); } } ! // URLStreamHandlerFactoryを設定 URL.setURLStreamHandlerFactory( new StubURLStreamHandlerFactory()); サンプル
  67. 67. Q. データベースのスローテストを解消したい
  68. 68. A. インメモリデータベースを使う
  69. 69. // DatabaseMapのinterfaceを実装 class MemoryDatabaseMap implements DatabaseMap { ! @Override public String getDriverClassName() { return JDBC.class.getName(); } ! @Override public String getConnectionString(File file) { // インメモリデータベースを使うように指定 return "jdbc:sqlite::memory:"; } ! @Override public String getMemoryConnectionString() { // インメモリデータベースを使うように指定 return "jdbc:sqlite::memory:"; } ! @Override public int getResultSetType() { return ResultSet.TYPE_FORWARD_ONLY; } ! @Override public String getSelectLastInsertIdentity() { return "SELECT last_insert_rowid() AS id"; } ! } サンプル
  70. 70. // UsingDatabaseMapで指定 @UsingDatabaseMap(MemoryDatabaseMap.class) @RunWith(RobolectricTestRunner.class) class DatabaseHelperTest { ... } サンプル
  71. 71. Q. Activityをテストしたい
  72. 72. A. ActivityControllerを使う
  73. 73. // ActivityControllerを生成 ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class); ! // Activityの生成 controller.create().start().resume().visible(); ! // Activityの取得 MainActivity activity = controller.get(); // Activityの操作 TextView textView = (TextView) activity .findViewById(R.id.text_view); ... ! // Activityの破棄 controller.pause().stop().destroy(); サンプル
  74. 74. Q. Serviceを検証したい
  75. 75. A. ライフサイクルに合わせてメソッドを呼び出す
  76. 76. private MyService mService; ! @Before public void setUp() { // Serviceを生成 mService = new MyService(); mService.onCreate(); } @Test public void test() { // Serviceを実行 Intent intent = new Intent(Intent.ACTION_SEARCH); intent.putExtra(SearchManager.QUERY, "Hello, World!"); mService.onStartCommand(intent, 0, 0); } ! @After public void tearDown() { // Serviceを破棄 mService.onDestroy(); } サンプル
  77. 77. Q. Widgetのテストをしたい
  78. 78. A. ShadowAppWidgetManagerを使う
  79. 79. // ShadowAppWidgetManagerの生成 Context context = Robolectric.application .getApplicationContext(); AppWidgetManager manager = AppWidgetManager.getInstance(context); ShadowAppWidgetManager shadowManager = Robolectric.shadowOf(manager); // Widgetの生成 int widgetId = shadowManager.createWidget( MyWidgetProvider.class, R.layout.activity_main); // Viewの取得 View widgetView = shadowManager.getViewFor(widgetId); ! ... サンプル
  80. 80. Q. ContentProviderのテストをしたい
  81. 81. A. ShadowContentResolverを使う
  82. 82. // ShadowContentResolverの生成 ContentResolver resolver = Robolectric.application.getContentResolver(); ShadowContentResolver shadowResolver = Robolectric.shadowOf(resolver); ! // ContentProviderの登録 shadowResolver.registerProvider( “com.example.android.unittest” new MyContentProvider()); ... // INSERTステートメントの取得 List<InsertStatement> insertStatements = shadowResolver.getInsertStatements(); // UPDATEステートメントの取得 List<UpdateStatement> updateStatements = shadowResolver.getUpdateStatements(); // DELETEステートメントの取得 List<DeleteStatement> deleteStatements = shadowResolver.getDeleteStatements(); // notifyChangeで通知されたURIの取得 List<NotifiedUri> notifiedUris = shadowResolver.getNotifiedUris(); サンプル
  83. 83. Q. BroadcastReceiverのテストをしたい
  84. 84. A. ShadowApplicationを使う
  85. 85. // 事前処理 ShadowApplication shadowApplication = Robolectric.getShadowApplication(); Context context = Robolectric.application.getApplicationContext(); // 登録済みBroadcastReceiverの取得 List<Wrapper> registered = shadowApplication.getRegisteredReceivers(); // Intentに該当するBroadcastReceiverの取得 Intent intent = new Intent("MY_ACTION"); List<BroadcastReceiver> receivers = shadowApplication.getReceiversForIntent(intent); // Intentを擬似的に受信 BroadcastReceiver receiver = receivers.get(0); receiver.onReceive(context, intent); サンプル
  86. 86. まとめ 4
  87. 87. まとめ 1. 課題はRobolectricで解消できる 2. 既存のライブラリを使って効率的に 3. 大体テストできるので書いてみよう
  88. 88. Q. おすすめの本はなんですか
  89. 89. https://github.com/t28hub/UnitTestInAndroid/

×