Atec mtg7 unittest

56
Testter(ユニットテスト的な意味で) ユニットテストの事例 2011.05.25 ATEC Mtg#7 2011526日木曜日

Transcript of Atec mtg7 unittest

Page 1: Atec mtg7 unittest

Testterを叩け!(ユニットテスト的な意味で)

ユニットテストの事例

2011.05.25 ATEC Mtg#7

2011年5月26日木曜日

Page 2: Atec mtg7 unittest

自己紹介

• Twitter id:@nowsprinting

• 名前:長谷川 孝二

• 職種:ワンマン社長(ワンマンバス的な意味で)

• 仕事:Java, Swing, Do-Ja, iOS(SIer/請負)

2011年5月26日木曜日

Page 3: Atec mtg7 unittest

Agenda

• 今日お話する範囲の定義(再確認)• JUnitの基礎的なこと

• Android Testing Framework の使用例

• Android Mock の使用例

2011年5月26日木曜日

Page 4: Atec mtg7 unittest

再確認

2011年5月26日木曜日

Page 5: Atec mtg7 unittest

©snskさん at ATEC Mtg#12011年5月26日木曜日

Page 6: Atec mtg7 unittest

©snskさん at ATEC Mtg#1

今日はここだけ

あえて除外(切り分けたい)

2011年5月26日木曜日

Page 7: Atec mtg7 unittest

具体的には?

2011年5月26日木曜日

Page 8: Atec mtg7 unittest

受け入れシステム

統合

ユニット

Activity遷移

ユースケース

Http通信

非機能要件

テストすべき内容(観点)の分布

ビジネスロジックLib正常系(Mock)

Lib異常系(Mock)

2011年5月26日木曜日

Page 9: Atec mtg7 unittest

受け入れシステム

統合

ユニット

Activity遷移

ユースケース

Http通信

非機能要件

テストすべき内容→テスト方法

Robolectric AndroidTest

MonkeyRunner

ビジネスロジックLib正常系(Mock)

Lib異常系(Mock)

God Hand

2011年5月26日木曜日

Page 10: Atec mtg7 unittest

受け入れシステム

統合

ユニット

Activity遷移

ユースケース

Http通信

非機能要件

テストすべき内容→テスト方法

Robolectric AndroidTest

MonkeyRunner

ビジネスロジックLib正常系(Mock)

Lib異常系(Mock)

God Hand

Android特有の観点・低メモリでActivity開放・移動機を縦→横に・着信→suspend→resume

・電波不安定(そろそろ検討着手を希望!)

2011年5月26日木曜日

Page 11: Atec mtg7 unittest

受け入れシステム

統合

ユニット

Activity遷移

ユースケース

Http通信

非機能要件

テストすべき内容→テスト方法

Robolectric AndroidTest

MonkeyRunner

ビジネスロジックLib正常系(Mock)

Lib異常系(Mock)

God Hand

今日はここだけ

InstrumentalTestCase系

2011年5月26日木曜日

Page 12: Atec mtg7 unittest

JUnitの基礎的なこと

• Android Testing FrameworkはJUnit3ベース

• TestCaseからプロダクトコードを呼ぶ

• TestCase内で、Assertionによって正否を判定する。

2011年5月26日木曜日

Page 13: Atec mtg7 unittest

package jp.group.android.atec.testter.logic;

public class TwitterLogicTest extends AndroidTestCase {}

単純なTestCaseの例

プロダクトコード

テストコード 同じpackage名(が便利)

Class名の後ろに”Test”JUnitのTestCaseを継承

package jp.group.android.atec.testter.logic;

public class TwitterLogic {}

2011年5月26日木曜日

Page 14: Atec mtg7 unittest

public void testTwitterLogicAuthorization() { Authorization auth4test = ...

TwitterLogic target = new TwitterLogic(auth4test);

assertNotNull("Twitterインスタンスが生成されていること", target.twitter);}

単純なテストメソッドの例“test” + 対象メソッド名 + ケース名

対象メソッド呼び出し

Assertion(用途にあったものを)

第一引数に評価内容を書く(推奨)

2011年5月26日木曜日

Page 15: Atec mtg7 unittest

実行結果(失敗)失敗!

赤い!

失敗!

assert()の第一引数

2011年5月26日木曜日

Page 16: Atec mtg7 unittest

実行結果(成功)失敗ゼロ!

グリーン!

2011年5月26日木曜日

Page 17: Atec mtg7 unittest

Android Testing Frameworkの使用例

2011年5月26日木曜日

Page 18: Atec mtg7 unittest

Test Project を作る

2011年5月26日木曜日

Page 19: Atec mtg7 unittest

eclipseの場合、package explorerで製品プロジェクトを選択して右クリックして、

New Test Project... を選択。

2011年5月26日木曜日

Page 20: Atec mtg7 unittest

Dialog (1/2)TestProject名

製品Project名(テスト対象)

下にスクロール2011年5月26日木曜日

Page 21: Atec mtg7 unittest

Dialog (2/2)

テストアプリケーションのpackage名

• 製品とテストを同じパッケージ名にしない。個別にインストールできなくなる。

• 各Classのpackage名とは別なので混同しない。

2011年5月26日木曜日

Page 22: Atec mtg7 unittest

Why?

2011年5月26日木曜日

Page 23: Atec mtg7 unittest

Android Testing Frameworkは、エミュレータ/実機にインストールされ実行されるアプリケーション ・・なので、

2011年5月26日木曜日

Page 24: Atec mtg7 unittest

Android Testing Frameworkは、エミュレータ/実機にインストールされ実行されるアプリケーション ・・なので、

実行まで、ちょっと時間がかかります・・・

2011年5月26日木曜日

Page 25: Atec mtg7 unittest

Android Testing Frameworkは、エミュレータ/実機にインストールされ実行されるアプリケーション ・・なので、

• 製品とテストを同じパッケージ名にしない。個別にインストールできなくなる。

• 各Classのpackage名とは別なので混同しない。こちらは同じpackage名にすることを推奨。

2011年5月26日木曜日

Page 26: Atec mtg7 unittest

TestCaseを書く

2011年5月26日木曜日

Page 27: Atec mtg7 unittest

Test Cases (1/2)• AndroidTestCase

• ApplicationTestCase

• LoaderTestCase

• ProviderTestCase2

• ServiceTestCase

※すべて、JUnitのTestCaseのサブクラス。いずれかをテストの 目的によって使い分ける。

2011年5月26日木曜日

Page 28: Atec mtg7 unittest

Test Cases (1/2)• AndroidTestCase

• ApplicationTestCase

• LoaderTestCase

• ProviderTestCase2

• ServiceTestCase

※すべて、JUnitのTestCaseのサブクラス。いずれかをテストの 目的によって使い分ける。

今のTestterにはこの辺を使う機能がない!

2011年5月26日木曜日

Page 29: Atec mtg7 unittest

Test Cases (2/2)• InstrumentationTestCase

• ActivityTestCase

• ActivityInstrumentationTestCase2

• ActivityUnitTestCase

• SingleLaunchActivityTestCase

• SyncBaseInstrumentation

2011年5月26日木曜日

Page 30: Atec mtg7 unittest

AndroidTestCase

2011年5月26日木曜日

Page 31: Atec mtg7 unittest

AndroidTestCase

• ロジックのテストに使う(非Activity)

• Contextを返す getContext() が利用可能(製品側のContextを得られる)

2011年5月26日木曜日

Page 32: Atec mtg7 unittest

public class PreferenceLogicTest extends AndroidTestCase {

public void testWriteReadToken() { PreferenceLogic logic = PreferenceLogic.getInstance();

logic.writeToken(getContext(), new AccessToken("11111", "22222")); AccessToken token = logic.readToken(getContext());

assertEquals("11111", token.getToken()); assertEquals("22222", token.getTokenSecret()); }

}

2011年5月26日木曜日

Page 33: Atec mtg7 unittest

ActivityInstrumentationTestCase2

2011年5月26日木曜日

Page 34: Atec mtg7 unittest

ActivityInstrumentationTestCase2

• Activityのテストに使う

• Activityの遷移を伴なうテストも可能。(今回は説明しません)

• Contextを返す getContext() が利用可能

• ActivityInstrumentationTestCaseは@Deprecated

2011年5月26日木曜日

Page 35: Atec mtg7 unittest

package jp.group.android.atec.testter;

public class AuthActivityTest extends ActivityInstrumentationTestCase2<AuthActivity> {

protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); mIntent = new Intent(); setActivityIntent(mIntent); mActivity = getActivity(); mEditText = (EditText) mActivity.findViewById(R.id.pinEdit); mButton = (Button) mActivity.findViewById(R.id.registBtn); }

2011年5月26日木曜日

Page 36: Atec mtg7 unittest

package jp.group.android.atec.testter;

public class AuthActivityTest extends ActivityInstrumentationTestCase2<AuthActivity> {

protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); mIntent = new Intent(); setActivityIntent(mIntent); mActivity = getActivity(); mEditText = (EditText) mActivity.findViewById(R.id.pinEdit); mButton = (Button) mActivity.findViewById(R.id.registBtn); }

Activityを生成

コンポーネントを取得製品packageのres

2011年5月26日木曜日

Page 37: Atec mtg7 unittest

public void testRegistBtnText() {// 登録ボタンのテキストが指定のものであることを確認する assertTrue(mButton.getText().toString() .equalsIgnoreCase("登録"));}

ふつうにAssertionで確認

テストメソッドでは、

2011年5月26日木曜日

Page 38: Atec mtg7 unittest

コンポーネントの操作も可能※但し、UIスレッドに限る

public void testClickButton() {

mActivity.runOnUiThread(new Runnable() { public void run() { try { mButton.performClick(); assertXxx(...); } catch (RuntimeException e) { assertXxx(...); } } });

}2011年5月26日木曜日

Page 39: Atec mtg7 unittest

privateを扱う

2011年5月26日木曜日

Page 40: Atec mtg7 unittest

製品コードの中のprivateなフィールド、メソッド

private final PreferenceLogic preferenceLogic = PreferenceLogic.getInstance();

private Drawable getCacheDrawable(URL url) { (snip)}

2011年5月26日木曜日

Page 41: Atec mtg7 unittest

リフレクションで取得することは可能

Class<AuthActivity> clazz = AuthActivity.class;Field field = clazz .getDeclaredField("preferenceLogic");

Method method = getClass() .getMethod("getCacheDrawable");method.invoke();

ですが、

2011年5月26日木曜日

Page 42: Atec mtg7 unittest

素人め、間合いが遠いわ!CV:井芹さん

2011年5月26日木曜日

Page 43: Atec mtg7 unittest

リフレクションの使用は Fragile Test の一因となる(テストのメンテナンスコストが上がる)ため、

private final PreferenceLogic preferenceLogic = PreferenceLogic.getInstance();

private Drawable getCacheDrawable(URL url) { (snip)}

製品コードをテストしやすく修正する。可視性をdefault(Package Private)にする等。

2011年5月26日木曜日

Page 44: Atec mtg7 unittest

※実際には、こんなテストをしたかった

Class<View> clazz = View.class;Field field = clazz.getDeclaredField("mOnClickListener");field.setAccessible(true);assertTrue(field.get(mButton) != null);

AndroidSDKの中のClassの、不可視なfield

→このケースは、Mockを使用する?

2011年5月26日木曜日

Page 45: Atec mtg7 unittest

例外のテスト

2011年5月26日木曜日

Page 46: Atec mtg7 unittest

プロダクトコード

public AccessToken auth(String pin) { AccessToken accessToken = null; try { (snip) } catch (TwitterException e) { (snip) throw new RuntimeException(e); } return accessToken;}

例外発生==異常系をテストしたい

2011年5月26日木曜日

Page 47: Atec mtg7 unittest

テストコード

try { target.auth(null); fail("Twitter#getOAuthAccessToken()で 例外が投げられるべき");} catch (RuntimeException e) { assertEquals("catchした例外の中身は正しい", expected, e.getCause());}

例外が発生しなければfail

正しい(想定通りの)例外か確認

2011年5月26日木曜日

Page 48: Atec mtg7 unittest

Android Mock

2011年5月26日木曜日

Page 49: Atec mtg7 unittest

Android Mockとは

• Dalvk上で使用できるEasyMockのラッパーです。

• Mockとは、本物のClassの振る舞い(メソッドの戻り値など)をエミュレートすることができる。

2011年5月26日木曜日

Page 50: Atec mtg7 unittest

製品コード

public AccessToken auth(String pin) { AccessToken accessToken = null; try { if (pin == null || "".equals(pin.trim())) { accessToken = twitter.getOAuthAccessToken(); } else { (snip) } } catch (TwitterException e) { (snip) } (snip)}

Twitter4Jのインスタンス

このメソッドの戻り値を変えたい2011年5月26日木曜日

Page 51: Atec mtg7 unittest

テストコード(正常系・1/2)

@UsesMocks(Twitter.class)public void testAuth_正常系_PINがnull() { // setup mock AccessToken expectedToken = new AccessToken("testToken", "testSecret");

Twitter twitterMock = AndroidMock.createMock(Twitter.class);

AndroidMock.expect(twitterMock .getOAuthAccessToken()).andReturn(expectedToken);

AndroidMock.replay(twitterMock);

アノテーションで宣言

Twitter型のMockを生成

メソッドに対する戻り値を設定

必要!2011年5月26日木曜日

Page 52: Atec mtg7 unittest

テストコード(正常系・2/2) // setup test target TwitterLogic target = new TwitterLogic(); target.twitter = twitterMock;

// do test. AccessToken actual = target.auth(null); assertEquals("返されるAccessTokenは正しい", expectedToken, actual);

// 呼ばれたMockメソッドの正当性を検証。 AndroidMock.verify(twitterMock);}

Mockをテスト対象にセット

Mockに定義したメソッドを呼ばれたかチェック

2011年5月26日木曜日

Page 53: Atec mtg7 unittest

テストコード(異常系)

@UsesMocks(Twitter.class)public void testAuth_異常系_認証失敗() { // setup mock TwitterException expected = new TwitterException("Unauthorized", null, 401); Twitter twitterMock = AndroidMock .createMock(Twitter.class); AndroidMock.expect(twitterMock .getOAuthAccessToken()).andThrow(expected);

AndroidMock.replay(twitterMock);

メソッドが呼ばれたら例外発生

2011年5月26日木曜日

Page 55: Atec mtg7 unittest

まとめ

• backlogのwiki嫁

• 井芹さんのスライド嫁

2011年5月26日木曜日

Page 56: Atec mtg7 unittest

ご静聴ありがとうございました!

2011.05.25 20:01

2011年5月26日木曜日