Android Espresso 初心者導入用のサンプル

Android

Android JUnit4 初心者導入用のサンプル]]というのを以前書きましたが、今回は、[[Espressoというフレームワークを使ったJUnitのテストを作ります。
前置きですが、自分自身初心者なので、「初心者用のサンプル」とはいささかおこがましいですが(^_^)、何か間違いがあれば、訂正していってください┌o ペコッ

まずは、gradle。
いっつもねー ここで必ず一波乱ある…。
もれなく今回も。
今回の問題は、該当のアプリのtargetSdkのバージョンを、22にしていたことです。
どうしても、22のままでは、espressoを読み込めなかった…orz
なので、targetSdkのバージョンを、23にしました。

下記のサイトのほぼコピペですが、appのレベルのbuild.gradleを次のように編集します。

https://developer.android.com/training/testing/start/index.html?hl=ja#config-instrumented-tests

 dependencies {
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    // Optional -- Hamcrest library
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
    // Optional -- UI testing with Espresso
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
    // Optional -- UI testing with UI Automator
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
 }
 android {
    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
 }

んで、次の問題。

 @RunWith(AndroidJUnit4.class)

 Cannot resolve symbol 'AndroidJUnit4'

で読み込めない。

もちろん、espressoさんも

 import static android.support.test.espresso.Espresso.onView;

 Cannot resolve symbol 'espresso'

で読み込めない。。。

これに結構はまりましたが、正解は、テスト用のコードの置き場所が違っていたのでした…。

app\src\testの下ではなく、app\src\androidTestの下にテストコードを置かないとダメなのです。

espresso.jpg

このあたり、どっかの設定とかで変更できるのかなー

よくわかりません。

で、やーっとテストできるかと思いきや!
上記の図にある、RegisterActivityTest.javaを右クリックして、Run RegisterActivityTestを押しても、

 Class not found:  "jp.onlineconsultant.hogehoge.RegisterActivityTest"Empty test suite.

となってしまい、テストできません。(つД`)

これも、jp.onlineconsultant.hogehogeのディレクトリのところを右クリックして、Run Tests in と書いてあるのをクリックすると、テストができます。

さあてさて、RegisterActivityTest.javaはこんな感じです。
初心者用の超シンプルなサンプルですので、ご容赦を。

 @RunWith(AndroidJUnit4.class)
 @LargeTest
 public class RegisterActivityTest {
 
    private RegisterActivity mRegisterActivity = null;
    private Button mRegisterButton;
    private EditText mLast_name_box;
 
    @Rule
    public ActivityTestRule<RegisterActivity> mActivityRule = new ActivityTestRule<>(
            RegisterActivity.class);
 
    @Before
    public void setUp(){
 
        onView(withId(R.id.register_button)).check(matches(withText("送信")));
 
    }
 
    //テストが動作するかだけの確認用
    @Test
    public void testGetCrowd() {
        Assert.assertEquals(1, 1);
    }
 }

Android Espresso パスワードなどのバリデーションチェック

Android

登録フォームなどのテスト、一番いやですよね~
あれやこれやと境界値を入力して、エラーメッセージを確認したり…。
手でやるとしんどいので、こういう時こそEspressoの出番です。

//RegisterActivity.java

 public class Validate {
 
    public static List<String> validateNormalRegisterInfo(HashMap<String, String> info, Context context){
 
        List<String> errorList = new ArrayList<>();
 
        if (! isEnoughCharacters(info.get("password"), 6)) {
            errorList.add(context.getString(R.string.error_password));
        }else if ( !info.get("password").matches("[0-9a-zA-Z]+")) {
            errorList.add(context.getString(R.string.error_password_novalid));
        }
 
        return errorList;
    }
 //ValidateTest.java
 public class  ValidateTest {
 
    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
 
    @Test
    public void isEnoughCharacter_ForPassword(){
 
        HashMap<String, String> map = new HashMap();
        map.put("password", "12345");
 
        List<String> testErrorList = new ArrayList<>();
 
        Activity activity = mActivityRule.getActivity();
        testErrorList.add(activity.getString(R.string.error_password));
        
        Assert.assertEquals(testErrorList, Validate.validateNormalRegisterInfo(map, activity));
    }
 
    @Test
    public void isValidCharacter_ForPassword(){
 
        HashMap<String, String> map = new HashMap();
        map.put("password", "うりぃぃぃぃぃぃ");
 
        List<String> testErrorList = new ArrayList<>();
 
        Activity activity = mActivityRule.getActivity();
        testErrorList.add(activity.getString(R.string.error_password_novalid));
 
        Assert.assertEquals(testErrorList, Validate.validateNormalRegisterInfo(map, activity));
    }
 
 }

あまり本題ではないのですが、テストコードからリソースを読みだすところ

 getString(R.string.error_password)

に地味に詰まりましたね。

まだまだEspresso、勉強中なので、こうすればいいよ!とかあれば、教えてください。┌o ペコッ

Android Espresso scrollToが効かない

Android
Espresso

Espressoの面倒なところが、UIで見えてないボタンなどについて次のようにテストしようとすると、

 onView(withId(R.id.face_photo_upload)).perform(scrollTo()).perform(click());
 android.support.test.espresso.PerformException: Error performing 'Send down motion event' on view 'unknown'.

とか

 is not displayed

とかでFailureになっちゃうことですね。

ソフトキーボードが開いていると、scrollToを指定しても、うまくスクロールされません。

なので、最初にソフトキーボードが開いちゃうような画面では、ソフトキーボードを閉じてから、テストするとうまくいきます。

 onView(withId(R.id.last_name)).perform(closeSoftKeyboard());   
 onView(withId(R.id.face_photo_upload)).perform(scrollTo()).perform(click());

Android Espresso cannot resolve intending

Android
Espresso

Espressoで、例えばあるアクティビティから、画像ファイルを取得して、戻る、というメソッドのテストをします。

Espresso 初心者用導入のチュートリアルURLで紹介したように、下記のURLによいサンプルがあるので、これを見て、やってみるぞ!となります。

https://codelabs.developers.google.com/codelabs/android-testing/index.html?index=..%2F..%2Findex&hl=ja#7

しかし、肝心の

 intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);

で、cannot resolve intending でintendingが見つかりません、となっちゃいます。

最初は、サンプルにあるように、AndroidTestMockとかに入れないといけないのかな?と思ってましたが、違いました。

下記にあるように、Espresso IntentsはEspressoの拡張なので、
https://google.github.io/android-testing-support-library/docs/espresso/intents/
appのbuild.gradleに、次の行を足します。

 androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'

これで、無事解決です!

Android Espresso AsyncでファイルをアップロードするAPIのテスト

Android

Android Espresso パスワードなどのバリデーションチェックなどでも書きましたが、登録フォームのテスト!これは時間がかかるし、嫌なものですよね~(;´Д`)

こういうことこそ、Espressoの出番です!
やることは、Androidアプリから、サーバーに登録情報をPostします。
ファイルを2つアップロード、Emailの重複は許可されていないので、Emailはランダムな文字列を作ります。
APIでEmailが重複していないので登録成功、Emailが重複しているので登録エラー、の2つの場合の返り値を読み取ってテストするところまでです。

CountDownLatchを使ってやってみましたが、もっとスマートな方法がありそうなんですけどね~。
毎回言っていますが、どなたか教えてください┌o ペコッ

 public class RegisterAsyncTest extends ActivityInstrumentationTestCase2 {
 
    String mJsonString = null;
    Exception mError = null;
    CountDownLatch signal = null;
    private Activity mActivity;
 
    public RegisterAsyncTest() {
        super(RegisterActivity.class);
    }
 
    @Override
    protected void setUp() throws Exception {
        signal = new CountDownLatch(1);
        mActivity = getActivity();
    }
 
    @Override
    protected void tearDown() throws Exception {
        signal.countDown();
    }
 
    public void testUploadTask() throws InterruptedException {
 
        HashMap<String, String> register_info = new HashMap<>();
 
        register_info.put("last_name", Constants.LAST_NAME);
        register_info.put("first_name", Constants.FIRST_NAME);
        register_info.put("addresses[0][post_code]", Constants.POST_CODE);
        register_info.put("addresses[0][prefecture]", Constants.PREFECTURE);
        register_info.put("addresses[0][city]", Constants.CITY);
        register_info.put("addresses[0][ward]", Constants.WARD);
        register_info.put("addresses[0][town]", Constants.TOWN);
        register_info.put("addresses[0][address]", Constants.ADDRESS);
        register_info.put("addresses[0][tel]", Constants.TEL);
        register_info.put("password", Constants.PASS);
 
        String email = makeRundomMailAddress();
        register_info.put("email", email);
        register_info.put("username",email);
 
        register_info.put("experience_years_from", Constants.EXPERIENCE);
        register_info.put("regular_login", Constants.REGULAR_LOGIN);
 
        register_info.put("licensePhotoFilepath", Constants.LICENCE_PHOTO_PATH);
        register_info.put("facePhotoFilePath", Constants.FACE_PHOTO_PATH);
 
        Context context = mActivity.getApplicationContext();
 
        UploadAsyncTask task = new UploadAsyncTask(context);
        task.setListener(new UploadAsyncTask.UploadTaskListener() {
            @Override
            public void onComplete(String jsonString, Exception e) {
                mJsonString = jsonString;
                mError = e;
                signal.countDown();
            }
        }).execute(register_info);
        signal.await();
 
        assertNull(mError);
        assertFalse(TextUtils.isEmpty(mJsonString));
 
        assertTrue(mJsonString.startsWith("{    \"response\": {        \"status\": \"SUCCESS\""));
        assertTrue(mJsonString.endsWith("}"));
 
    }
 
    /**
     * すでにあるメールアドレスではエラーが帰ってくる
     * @throws InterruptedException
     */
    public void testUploadTaskEmailInvalidFail() throws InterruptedException {
 
        HashMap<String, String> register_info = new HashMap<>();
 
 
        register_info.put("last_name", Constants.LAST_NAME);
        register_info.put("first_name", Constants.FIRST_NAME);
        register_info.put("addresses[0][post_code]", Constants.POST_CODE);
        register_info.put("addresses[0][prefecture]", Constants.PREFECTURE);
        register_info.put("addresses[0][city]", Constants.CITY);
        register_info.put("addresses[0][ward]", Constants.WARD);
        register_info.put("addresses[0][town]", Constants.TOWN);
        register_info.put("addresses[0][address]", Constants.ADDRESS);
        register_info.put("addresses[0][tel]", Constants.TEL);
        register_info.put("password", Constants.PASS);
 
        String email = "hogeo@onlineconsultant.jp";
        register_info.put("email", email);
        register_info.put("username",email);
 
        register_info.put("experience_years_from", Constants.EXPERIENCE);
        register_info.put("regular_login", Constants.REGULAR_LOGIN);
 
        register_info.put("licensePhotoFilepath", Constants.LICENCE_PHOTO_PATH);
        register_info.put("facePhotoFilePath", Constants.FACE_PHOTO_PATH);
 
        Context context = mActivity.getApplicationContext();
 
        UploadAsyncTask task = new UploadAsyncTask(context);
        task.setListener(new UploadAsyncTask.UploadTaskListener() {
            @Override
            public void onComplete(String jsonString, Exception e) {
                mJsonString = jsonString;
                mError = e;
                signal.countDown();
            }
        }).execute(register_info);
        signal.await();
 
        assertNull(mError);
        assertFalse(TextUtils.isEmpty(mJsonString));
 
        assertTrue(mJsonString.startsWith("{    \"response\": {        \"info\": \"email:"));
        assertTrue(mJsonString.endsWith("}"));
 
    }
 
 }