unit test in android
DESCRIPTION
Unit test in Android using Robolectric.TRANSCRIPT
ユニットテスト入門Androidアプリケーションへのユニットテスト導入
Tatsuya MakiAndroid Application Developer
はなすこと1. ユニットテストの導入
2. 効率的なユニットテスト
3. ユニットテストのTips
ユニットテストの導入
1
Q. ユニットテストとは
A. メソッドなど小さな単位で行うテスト
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
@Test public void fizzbuzzShouldReturnBuzzWhenValueIs10() { // 実行結果 String actualValue = fizzbuzz(10); ! // 期待結果 String expectedValue = "Buzz"; ! // テスト assertThat(actualValue, is(expectedValue)); }
FizzBuzzTest
Androidでの問題点1. デバイスが必須
2. 実行速度が遅い
3. テストしにくい
Robolectrichttp://robolectric.org/
遅い
DalvikVM
必要
JUnit3
速い
JVM
不要
JUnit4
AndroidTestCase
実行速度
VM
デバイス
JUnit
Robolectric
TextView Shadow
getText()
“Hello, world.”
@Implements(TextView.class) class MyShadowTextView extends ShadowView { ! @Implementation public CharSequence getText() { return "Hello, Robolectric!"; } !}
Shadowの例
@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の利用例
まとめ1. デバイスが不要
2. JVM上で動作する
3. Shadowオブジェクト
効率的なユニットテスト
2
3つのツール1. FEST
2. Mockito
3. EclEmma
FESThttps://code.google.com/p/fest/
Q.アサーションとは
A. 実行結果と期待結果を比較検証する宣言
// 実行結果 int actualValue = 10 / 2; !// 期待結果 int expectedValue = 5; !// "10 / 2"が"5"と等しくなることを表明 assertEquals(actualValue, expectedValue);
アサーションの例
FESTのメリット1. アサーションの記述が容易
2. エラーメッセージが明確
3. Androidと親和性が高い
アサーションの記述が容易
// 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
// 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
// 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
エラーメッセージが明確
// テストコード List<String> devices = new ArrayList<String>(); devices.add("Nexus 5"); devices.add("Nexus 7"); assertTrue(devices.contains("Nexus 4")); !!// エラーメッセージ java.lang.AssertionError !!!!
JUnit
// テストコード 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
// テストコード 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
Androidと親和性が高い
// テストコード 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
// テストコード 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
Mockitohttp://mockito.org/
Q. モックオブジェクトとは
A. オブジェクトの呼び出しを検証する
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; } !}
モックオブジェクトの例
Mockitoでできること1. 呼び出しの検証
2. 振る舞いの変更
3. フィールドの変更
// モックオブジェクトの生成 InputStream mocked = mock(InputStream.class); // InputStreamをクローズする mocked.close(); !// closeメソッドの呼び出し検証 verify(mocked).close();
呼び出しの検証
振る舞いの変更
// モックオブジェクトの作成 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; } });
フィールドの変更
// 内部にInputStreamを保持するクラス MyObject object = new MyObject(); // フィールドの取得 InputStream stream = (InputStream) Whitebox .getInternalState(object, “mStream"); !// スパイオブジェクトの作成 Socket spied = spy(stream); !// フィールドの変更 Whitebox .setInternalState(object, "mStream", spied);
Mockitoでできないこと1. finalクラス/メソッドのモック
2. privateメソッドのモック
3. staticメソッドのモック
EclEmmahttp://www.eclemma.org/
Q. カバレッジとは
A. テストがどれだけ網羅できているか
カバレッジの種類C0: 命令網羅率
C1: 分岐網羅率
C2: 条件網羅率
EclEmmaで測定できるものC0: 命令網羅率
C1: 分岐網羅率
C2: 条件網羅率
EclEmmaで測定できないものC0: 命令網羅率
C1: 分岐網羅率
C2: 条件網羅率
ユニットテストのTips
3
Q. void型メソッドをテストしたい
A. 別のメソッドを使って検証する
List<String> list = new MyList<String>(); list.add("Android"); // 別のメソッドで検証 assertThat(list).hasSize(1);
サンプル
A. メソッドの呼び出しを検証する
InputStream mocked = mock(InputStream.class); IoUtils.close(mocked); !// closeメソッドの呼び出し検証 verify(mocked).close();
サンプル
Q. privateメソッドをテストしたい
A. 諦める
A. package privateに変更する
// privateメソッドなので呼び出せない private String buildMessage() { String message; // do something return message; } !// package privateに変更 String buildMessage() { String message; // do something return message; }
サンプル
Q. 非同期処理をテストしたい
A. CountDownLatchを使う
// 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(); ! ... }
サンプル
Q. HTTP通信の外部依存をなくしたい
A. FakeHttpLayerを使うHttpClientの場合
// 常に同じレスポンスを返却 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());
サンプル
A. URLStreamHandlerを設定するHttpURLConnectionの場合
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());
サンプル
Q. データベースのスローテストを解消したい
A. インメモリデータベースを使う
// 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"; } !}
サンプル
// UsingDatabaseMapで指定 @UsingDatabaseMap(MemoryDatabaseMap.class) @RunWith(RobolectricTestRunner.class) class DatabaseHelperTest { ... }
サンプル
Q. Activityをテストしたい
A. ActivityControllerを使う
// 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();
サンプル
Q. Serviceを検証したい
A. ライフサイクルに合わせてメソッドを呼び出す
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(); }
サンプル
Q. Widgetのテストをしたい
A. ShadowAppWidgetManagerを使う
// 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); !...
サンプル
Q. ContentProviderのテストをしたい
A. ShadowContentResolverを使う
// 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();
サンプル
Q. BroadcastReceiverのテストをしたい
A. ShadowApplicationを使う
// 事前処理 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);
サンプル
まとめ
4
まとめ1. 課題はRobolectricで解消できる
2. 既存のライブラリを使って効率的に
3. 大体テストできるので書いてみよう
Q. おすすめの本はなんですか
https://github.com/t28hub/UnitTestInAndroid/