2015年4月26日日曜日

IntentsTestRuleを使って、Intentのテストを行う

IntentsTestRuleについて

IntentsTestRuleは、startActivity()でセットしたIntentのデータをテストすることができます。
このクラスはActivityTestRuleを継承したクラスで、各Testの実行前にEspresso-Intentsを初期化し、実行後にEspresso-Intentsをリリースします。
各テスト後にActivityはfinish()されます。

build.gradleにEspresso-Intentsライブラリを追加します。
    androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.1'

サンプルソース

次はユーザー操作によって、Intent.ACTION_CALLのIntentが発生したかどうかテストするサンプルプログラムです。
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityIntentTest {

    @Rule
    public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
            MainActivity.class);

    @Before
    public void stubAllExternalIntents() {
        // By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before
        // every test run. In this case all external Intents will be blocked.
        intending(not(isInternal()))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));
    }

    @Test
    public void callPhone() {
        // call action
        onView(withId(R.id.callButton)).perform(click());

        // test
        intended(allOf(
                hasAction(Intent.ACTION_CALL),
                hasData("tel:0123456789"),
                toPackage("com.android.server.telecom")));

    }
}
テストクラスは次のように作成します。
  • テストクラスにRunWithアノテーションをつける
  • IntentsTestRuleを生成する、Ruleアノテーションをつける

Intentが発生したかどうかは、次のように判定します。
  • intendedメソッドで意図したIntentが発生しているかチェックする
  • matchメソッドはこちらを参照


  • Activity起動時のIntentセット

    テストを行うActivityの起動用Intentが必要な場合、IntentsTestRule#getActivityIntentメソッドをオーバーライドしてIntentを返す。
        @Rule
        public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class) {
    
            /**
            * Activity起動用Intent
            */
            @Override
            protected Intent getActivityIntent() {
                Intent intent = new Intent();
                // Activity起動用のパラメータをセット
                intent.putExtra(KEY_DATA,data);
                return intent;
            }
        };
    

    参考サイト:
    https://github.com/googlesamples/android-testing

    2015年3月1日日曜日

    EspressoでToast表示のチェックをする

    公式ページにアプリケーションLayer以外のチェック方法が載っています。
    Using inRoot to target non-default windows
    https://code.google.com/p/android-test-kit/wiki/EspressoSamples#Using_inRoot_to_target_non-default_windows

    onView(withText("South China Sea"))
      .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
      .perform(click());
    

    上記の方法でもToast表示のチェックは行えますが、「non-default windows」であってToastの指定ではありません。
    ということで、自作してしまいましょう。

    ToastはWindowManager.LayoutParams.TYPE_TOASTというパラメータを持っています。
    これは表示するLayerを指定するものです。
    次はToast Layerを指定するMatcherのサンプルプログラムです。

        /**
         * Matcher that is Toast window.
         */
        public static Matcher<Root> isToast() {
            return new TypeSafeMatcher<Root>() {
    
                @Override
                public void describeTo(Description description) {
                    description.appendText("is toast");
                }
    
                @Override
                public boolean matchesSafely(Root root) {
                    int type = root.getWindowLayoutParams().get().type;
                    if ((type == WindowManager.LayoutParams.TYPE_TOAST)) {
                        IBinder windowToken = root.getDecorView().getWindowToken();
                        IBinder appToken = root.getDecorView().getApplicationWindowToken();
                        if (windowToken == appToken) {
                            // windowToken == appToken means this window isn't contained by any other windows.
                            // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
                            return true;
                        }
                    }
                    return false;
                }
            };
        }
    

    EspressoのViewMatchersモジュールを自作する

    EspressoのViewMatchersクラスには、たくさんの判定モジュールが用意されています。
    ただ、JUnit作成時に「こんなチェックをしたいのに、標準で用意されていない」こともあります。
    このような場合、自作するしかありません。

    次のような作業が必要です。
    1. BoundedMatcherを実装したオブジェクトを戻り値とするモジュールを作成
    2. マッチしているかどうか判定するモジュール、matchesSafelyをオーバーライドして判定ロジックを作成
    3. 何を判定するMatcherなのかを記述するdescribeToをオーバーライドして実装

    Espressoのソースが公開されていますので、参考になります。
    ViewMatchersクラス:https://code.google.com/p/android-test-kit/source/browse/espresso/lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchers.java

    次はTextViewのColorが一致しているかどうか判定するMatcherのサンプルプログラムです。
    public static Matcher<view> withTextColor(final int resourceId) {
    
            return new BoundedMatcher<view textview="">(TextView.class) {
                private int expectedColor = -1;
    
                private String resourceName;
    
    
                @Override
                protected boolean matchesSafely(TextView textView) {
                    if (expectedColor == -1) {
                        try {
                            expectedColor = textView.getResources().getColor(resourceId);
                            resourceName = textView.getResources().getResourceEntryName(resourceId);
                        } catch (Resources.NotFoundException ignored) {
                            // view could be from a context unaware of the resource id.
                        }
                    }
    
                    if (expectedColor != -1) {
                        return (expectedColor == textView.getCurrentTextColor());
                    } else {
                        return false;
                    }
                }
    
                @Override
                public void describeTo(Description description) {
                    description.appendText("with color from resource id: ");
                    description.appendValue(resourceId);
                    if (null != resourceName) {
                        description.appendText("[");
                        description.appendText(resourceName);
                        description.appendText("]");
                    }
                    if (-1 != expectedColor) {
                        description.appendText(" value: ");
                        description.appendText(String.valueOf(expectedColor));
                    }
                }
            };
        }
    

    2015年2月26日木曜日

    Swiftでパラメータ付きPOSTリクエストを行う

    SwiftでWebAPIのリクエストを行う際、POSTメソッドを使う必要がありました。
    次は非同期でリクエストを行う、NSURLConnectionクラスのsendAsynchronousRequestメソッドを用いたサンプルソースです。

            // URLセット
            let url = NSURL(string: "https://test.url.jp")
            var request : NSMutableURLRequest = NSMutableURLRequest(URL: url!)
    
            // POSTメソッド指定
            request.HTTPMethod = "POST"
            
            // POSTパラメータ
            var bodyData: String = "key1=value1&key2=value2"
            request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
            // ヘッダの指定
            request.setValue("HeaderValue", forHTTPHeaderField: "HeaderName")
            
            //asyncで実行
            NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: responseHandler)
    

    2015年2月24日火曜日

    SwiftなCocoa Touch FrameworkでCommonCryptoを使う

    Swiftを使ってCocoa Touch Frameworkを作成しています。
    どうしても、CommonCryptoを使う必要があり手段を探していました。

    以下、参考になったサイトです。
    Importing CommonCrypto in a Swift framework
    CommonHMAC in Swift

    手順は次のようになります。

    1. CommonCryptoというディレクトリを作成
    2. module.mapというファイルを作成
    3. プロジェクト設定のBuild Settings -> Swift Compiler - Search Paths -> Import Pathsに、上記のCommonCryptoディレクトリを指定



    次はサンプルです。 module.mapにSDK内のヘッダファイルのPathを記述します。

    module CommonCrypto [system] {
        header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/CommonCrypto/CommonCrypto.h"
        link "CommonCrypto"
        export *
    }
    

    このあたり、公式Developerサイトに書いてるのかな?

    2015年2月21日土曜日

    Unit testで非同期処理のWaitをする

    XcodeでiOS開発の勉強をはじめました。
    いきなりですが、UnitTestネタです。テストしないもの、エンジニアにあらずです。

    参考:Writing Test Classes and Methods

    waitForExpectationsWithTimeoutで非同期処理の待ち状態を指定

    標準テストフレームワークのXCTestExpectationとXCTestCase.waitForExpectationsWithTimeoutを組み合わせて実装します。
    手順は次のようになります。

    1. XCTestCase.expectationWithDescriptionをコールして、XCTestExpectationを取得 
    2. XCTestCase.waitForExpectationsWithTimeoutで、waitを指定 
    3. 非同期処理終了のタイミングで、XCTestExpectation.fulfill()。もし、waitForExpectationsWithTimeoutで指定した時間内にコールしなければfailとなる

    サンプルプログラムです。
        func testPerformAsyncRequest(){
            // XCTestExpectationの取得
            let expectation = self.expectationWithDescription("client key")
    
            // 非同期処理のコールバック処理完了後、expectation.fulfill()をコール
            expectation.fulfill()
            
            //waitの時間指定
            self.waitForExpectationsWithTimeout(5, handler: nil)
            
        }
    

    2015年2月8日日曜日

    コードカバレッジを使って、視覚的/統計的にテスト状況を確認する

    build.gradleの設定

    Code Coverage機能を使用するために、app直下のbuild.gradleに追加します。

    android {
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
            }
    
    +        debug {
    +            testCoverageEnabled = true
    +        }
        }
    }
    

    複数のProjectをImportしている場合、各モジュールのbuild.gradleにtestCoverageEnabledを追加します。

    Code Coverageの実行

    Terminalから次のコマンドを実行します。

    ./gradlew createDebugCoverageReport
    

    createDebugCoverageReportを実行すると、自動的にandroidTestを行います。
    デバイス(or エミュレータ)が起動していない場合、次のようなエラーが表示されます。

    :app:connectedAndroidTest FAILED          
                  
    FAILURE: Build failed with an exception.
                  
    * What went wrong:
    Execution failed for task ':app:connectedAndroidTest'.
    > com.android.builder.testing.api.DeviceException: java.lang.RuntimeException: No connected devices!
    

    必ずデバイスかエミュレータを起動して、androidTestを実行できる環境にしておきましょう。

    レポートの観覧

    createDebugCoverageReportが成功すると、各モジュールのOutputにレポートを保存します。
    [PROJECT]/app/build/outputs/reports/coverage/debug/index.html

    次のような表が出力されます。コードの全ライン数に対して、テストが実行されたラインが統計的に確認する事ができます。



    次のように視覚的に、どのコードがテストされていないか確認することも可能です。