このエントリーは、GMOアドマーケティング Advent Calendar 2018 の 12/15の記事です。
GMOアドマーケティングとしては初のAdvent Calendar参戦です。
こんにちは。
GMOアドマーケティングのT.Mです。
はじめに
広告SDKを組み込んだアプリのテストを人手で行っているのですが、
可能な限り自動化したいと思い、UIテストの自動化を試みています。
AndroidにはUIテストを行う方法として、主に3つのフレームワークがあります。(もっとありますが)
- Espresso
- UI Automator
- appium
各フレームワークを簡単に調査
Espresso
GoogleのUIテストフレームワークです。
単一のアプリを対象にしていて、ホワイトボックステストが可能です。
そのため、アプリのプロジェクトにテストコードを書いていきます。
Androidのドキュメントがあります。
Test UI for a single app
UI Automator
こちらもGoogleのUIテストフレームワークです。
複数のアプリ間のやりとりを含むテストが可能です。ブラックボックステストになります。
自分のアプリ以外も操作できるので、設定アプリから機内モードにするとかも可能です。
こちらもAndroidのドキュメントがあります。
Test UI for multiple apps
appium
Android、iOS、Windowsのクロスプラットフォームで使用できるOSSのオートメーションライブラリです。
Seleniumと同様にWebDriverを使用します。
Android環境では、内部でUI Automatorが使用されています。(4.2以上)
テスト対象のアプリ
テスト対象となるアプリです。
JSONを返すサーバにGETリクエストして取得した内容を表示します。
ライセンス表記
テストアプリ作成に伴いライブラリを使っていますのでライセンス表記をします。
OkHttp、Picasso、android-testingは、Apache License, Version 2.0です。
This software includes the work that is distributed in the Apache License 2.0
json-serverはMITライセンスです。
https://github.com/typicode/json-server#license
JSONサーバ
サンプルなので簡単にJSON Serverを使います。
npmで一発でインストール出来るので簡単です。
用意するJSONは下記のようにしました。
タイトル、詳細、画像URLです。
1 2 3 4 5 |
{ "ad": [ { "id": 1, "title": "TITLE", "description": "DESCRIPTION", "img-url": "https://techblog.gmo-ap.jp/wp-content/uploads/2016/03/logo_1.png" } ] } |
ローカルPCで動かすので、そのまま起動するとlocalhost:3000になるのですが、
Androidエミュレータからだとlocalhostにアクセスできませんでした。
そのため、ローカルPCのIPアドレス(例えば192.168.1.7)をホスト名に指定して起動します。
1 |
$ json-server --watch db.json --host 192.168.1.7 |
http://192.168.1.7:3000/ad/1
にアクセスするとJSONが取得できます。
サンプルアプリ
JSONサーバにGETリクエストしてJSONを取得し、画面に表示します。
OkHttpとPicassoを使って簡単に作ります。
ライブラリを設定します。
1 2 |
implementation 'com.squareup.okhttp3:okhttp:3.12.0' implementation 'com.squareup.picasso:picasso:2.71828' |
インターネットに接続できるようにパーミッションを設定します。
1 |
<uses-permission android:name="android.permission.INTERNET"/> |
画面レイアウトです。
ボタンを押したらリクエストします。
title、descriptionを表示するTextViewに、
img-urlの画像を表示するImageViewを配置しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/load" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/logo" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TextView" /> <TextView android:id="@+id/description" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TextView" /> </LinearLayout> </LinearLayout> </android.support.constraint.ConstraintLayout> |
ロジック部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
package com.example.testsample; import android.os.Handler; import android.os.Looper; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import com.squareup.picasso.Picasso; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class MainActivity extends AppCompatActivity { private final OkHttpClient client = new OkHttpClient(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button loadButton = findViewById(R.id.load); loadButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Viewに表示するのでUIスレッドにアクセス出来るようにします。 final Handler handler = new Handler(Looper.getMainLooper()); Request request = new Request.Builder() .url("http://192.168.1.7:3000/ad/1") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { // レスポンスのJSONをパースします。 String body = responseBody.string(); JSONObject json = new JSONObject(body); final String title = json.getString("title"); final String description = json.getString("description"); final String imgUrl = json.getString("img-url"); handler.post(new Runnable() { @Override public void run() { ImageView imageView = findViewById(R.id.logo); TextView titleView = findViewById(R.id.title); TextView descriptionView = findViewById(R.id.description); // 画面に表示します。 Picasso.get() .load(imgUrl) .into(imageView); titleView.setText(title); descriptionView.setText(description); } }); } catch (JSONException e) { e.printStackTrace(); } } }); } }); } } |
実行して、ボタンをクリックすると取得した内容が表示されます。
Espressoを使ってみる
ようやく本題です。
今回はEspressoを使ってみたいと思います。
まずはライブラリの設定です。
最近のAndroidStudioで新規作成すると、runnerとespresso-coreは最初から入っていました。
すでに入っているようであれば、rulesだけ追加します。
1 2 3 |
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:rules:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' |
テストコードを作成します。
app/src/androidTest/java/パッケージ名/の下に配置します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package com.example.testsample; import android.support.test.rule.ActivityTestRule; import org.junit.Rule; import org.junit.Test; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static com.macchan_dev.testsample.ImageViewHasDrawableMatcher.hasDrawable; import static org.hamcrest.Matchers.not; public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class); @Test public void 表示テスト() throws Exception { // ImageViewに画像がないことを確認 onView(withId(R.id.logo)).check(matches(not(hasDrawable()))); // ボタンをクリックします onView(withId(R.id.load)).perform(click()); // JSONサーバへリクエストして表示するまでスリープを入れます。 Thread.sleep(1000); // TextEditにtitleが入っているか確認します。 onView(withId(R.id.title)).check(matches(withText("TITLE"))); // TextEditにdescriptionが入っているか確認します。 onView(withId(R.id.description)).check(matches(withText("DESCRIPTION"))); // ImageViewに画像が入っていることを確認します。 onView(withId(R.id.logo)).check(matches(hasDrawable())); } } |
onView()・・・ViewInteractionを生成します。ここからViewをクリックしたり内容のチェックが可能になります。
withId()・・・リソースIDからMatcherを生成します。
check()・・・引数で指定したアサーションの確認をします。
matches()・・・ViewAssertionを生成します。これによりcheck()ができます。
perform()・・・Viewに対して操作します。
click()・・・Viewをクリックします。
withText()・・・文字列からMatcherを生成します。
他にもいろいろな事が出来るのでEspressoのリファレンスを参照してください。
hasDrawable()は、Espressoには用意されておらず新規で作成しています。
これはGoogleのサンプルソースから持ってきています。
ImageViewのmDrawableの有無を確認するMatcherになっています。
このテストプログラムを実行するとボタンをクリックし、1秒待って、各要素のチェックを行います。
おわりに
Espressoを使ってテストを書きました。
今回は単純にViewの要素を見ているだけですが、IntentやWebViewのテストも可能です。
テストプログラムでJSONサーバへリクエストしてからsleepを入れているので、
もっとスマートな方法はないかなど確認していきたいです。
UI Automatorやappiumで書くとどう違うのか試していきたいと思います。
以上です。
明日は「座標降下法による行列分解の実装について」のお話です。
お楽しみに!
クリスマスまで続くGMOアドマーケティング Advent Calendar 2018
ぜひ今後も投稿をウォッチしてください!
■エンジニアによるTechblog公開中!
https://techblog.gmo-ap.jp/
■Wantedlyページ ~ブログや求人を公開中!~
https://www.wantedly.com/projects/199431
■エンジニア採用ページ ~福利厚生や各種制度のご案内はこちら~
https://www.gmo-ap.jp/engineer/
■エンジニア学生インターン募集中! ~有償型インターンで開発現場を体験しよう~
https://hrmos.co/pages/gmo-ap/jobs/0000027