私達の会社では、Playストア掲載用の画像はデザインの担当に作ってもらっています。
で、「特定の端末でだけ、見にくいよ〜」などのフィードバックが必要であればしたいところです。
ただ、そのためだけに確認用端末を買うわけにはいきませんね。
ということで、最近でたAWS Device farmを使って、撮ってみましょう、と思い立ったのです。
個人的に、へんなDSL使うのは好きではないのと、
そもそもテスト対象はアプリではないので、
uiautomatorを使います。
ざっくりいうと
- Google Playで、スクリーンショットを撮りたいターゲットのページにはちゃんと遷移できているっぽい
- スクリーンショットを撮って、保存すること自体はうまくいっているっぽい
- AWS Device Farmのドキュメントに書いてあるパスにスクリーンショットを配置してみたが、AWS Device Farmのシステムがそれを拾ってくれない。
- ドキュメントに書いてあるやり方でダメなら、自前でDropboxにあげてやろうじゃないの! としたが、 HTTP POSTのさいに勝手にコネクションリセットされてしまい、アップロードできなかった
- Keep-Aliveをやめたら、アップロードできるようになった!
- Google アカウントが設定されていない!( ゚д゚) ←イマココ
テストプロジェクトを作成
Android Studioで適当な新規プロジェクトを作ります。
本体(はろーわーるど)は、特に用事はないので、何もいじりません。
Dropboxにファイルはアップロードしようかなと思い、INTERNETパーミッションをつけるところだけはいじりました。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.crowdworks.awssample"
android:sharedUserId="jp.co.crowdworks.awssample.uid">
<uses-permission android:name="android.permission.INTERNET"/>
<application
uiautomator 2.0を使います。
あと、DropboxにあげるにはHTTPクライアントとして、今回はOkHttp使いました。
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
// Set this dependency to use JUnit 4 rules
androidTestCompile 'com.android.support.test:rules:0.3'
// Set this dependency to build and run Espresso tests
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
// Set this dependency to build and run UI Automator tests
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
// for uploading screenshots into Dropbox.
androidTestCompile 'com.squareup.okhttp:okhttp:2.4.0'
}
テストを書きます。
注意点としては、uiautomator 2.0は InstrumentationTestだということです。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.crowdworks.awssample.test"
android:sharedUserId="jp.co.crowdworks.awssample.uid">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
public class ApplicationTest extends InstrumentationTestCase {
private UiDevice mDevice;
private Context mContext;
@Override
protected void setUp() throws Exception {
super.setUp();
// Initialize UiDevice instance
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mContext = getInstrumentation().getTargetContext();
}
private File ensureDir(File d){
if(!d.exists()){
d.mkdir();
}
return d;
}
public void test1PlayStoreScreenshot() {
// Start from the home screen
mDevice.pressHome();
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=jp.co.crowdworks.androidapp"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
try{Thread.sleep(10*1000);}catch(Exception e){}
File screenshotDir = ensureDir(new File(mContext.getFilesDir().getParentFile(),"app_spoon-screenshots"));
File screenshotDirCls = ensureDir(new File(screenshotDir,ApplicationTest.class.getName()));
File screenshotDirMethod = ensureDir(new File(screenshotDirCls,"test1PlayStoreScreenshot"));
File fileA = new File(screenshotDirMethod,"/a.png");
File fileB = new File(screenshotDirMethod,"/b.png");
mDevice.takeScreenshot(fileA);
try{Thread.sleep(5*1000);}catch(Exception e){}
try {
mDevice.setOrientationLeft();
} catch(Exception e){}
try{Thread.sleep(5*1000);}catch(Exception e){}
mDevice.takeScreenshot(fileB);
try{Thread.sleep(5*1000);}catch(Exception e){}
assertTrue(fileA.exists());
assertTrue(fileB.exists());
}
public void test2UploadScreenshotFile() {
File screenshotDir = ensureDir(new File(mContext.getFilesDir().getParentFile(),"app_spoon-screenshots"));
File screenshotDirCls = ensureDir(new File(screenshotDir,ApplicationTest.class.getName()));
File screenshotDirMethod = ensureDir(new File(screenshotDirCls,"test1PlayStoreScreenshot"));
File fileA = new File(screenshotDirMethod,"/a.png");
File fileB = new File(screenshotDirMethod,"/b.png");
assertTrue(fileA.exists());
assertTrue(fileB.exists());
upload(fileA);
upload(fileB);
}
private void upload(File file){
final OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder()
.url("https://api-content.dropbox.com/1/files_put/auto/"+ Build.MODEL+"_"+file.getName())
.addHeader("Authorization", "Bearer XXXXXXXXXXXXXX(DropboxのAPIトークン)XXXXXXXXXXXXXX")
.post(RequestBody.create(MediaType.parse("image/png"),file))
.build();
try {
Response response = client.newCall(req).execute();
assertTrue(response.isSuccessful());
}
catch(IOException e){
Log.e("hoge","error",e);
assertTrue(false);
}
}
@Override
protected void tearDown() throws Exception {
mDevice.unfreezeRotation();
super.tearDown();
}
}
test1PlayStoreScreenshot()のなかで、やたらと深い階層にスクリーンショットを保存していますが、なぜこんなところに置いているかというと、
こんなかんじでAWSのマニュアルに書いてあるからです。
AWSとしては、Spoonというライブラリで保存することを期待しているんだと思います。
public static File screenshot(Activity activity, String tag) {
StackTraceElement testClass = findTestClassTraceElement(Thread.currentThread().getStackTrace());
String className = testClass.getClassName().replaceAll("[^A-Za-z0-9._-]", "_");
String methodName = testClass.getMethodName();
Spoon-Clientをみてみると、↑のような実装になっているので、
/data/data/アプリのパッケージ名/app_spoon-screenshots/テストパッケージ名.テストクラス名/テストメソッド名/
配下にスクリーンショットを保存すれば良い、というわけです。
テストプロジェクトをビルド
./gradlew clean assembleDebug assembleDebugAndroidTest
すると、成果物が、
- app/build/outputs/apk/app-debug.apk
- app/build/outputs/apk/app-debug-androidTest-unaligned.apk
みたいなかんじでできるかと思います。こいつをAWS Device Farmにアップロードして、テストを走らせます。
ローカルで試験が通ることを確認する
実機をUSB接続し、
adb install -r app/build/outputs/apk/app-debug.apk
adb install -r app/build/outputs/apk/app-debug-androidTest-unaligned.apk
adb shell am instrument -w jp.co.crowdworks.awssample.test/android.support.test.runner.AndroidJUnitRunner
を実行すると、スクリーンショットが縦・横あわせてDropboxにアップロードされることを確認。
AWS Device FarmにAPKを上げて実行する
ローカル確認がうまくいったところで、AWSに上げます。
app-debug.apkをアップします。
↓
uiautomator 2.0はInstrumentationTestなので、
Instrumentationを選択してapp-debug-androidTest-unaligned.apkをアップします。
↓
デバイスを選択する画面ですが、面倒なのでデフォルトのままとします。
↓
あとは、「Next step」を連打。
さて、テストが始まります。
→ test1PlayStoreScreenshot はテストが通る = スクリーンショットはちゃんととれている
→ test2UploadScreenshotFile はテストにFailする。logcatを見てみると、
08-06 00:29:47.445 23838 23852 E hoge : error
08-06 00:29:47.445 23838 23852 E hoge : java.io.IOException: unexpected end of stream on Connection{api-content.dropbox.com:443, proxy=DIRECT@ hostAddress=184.72.246.165 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} (recycle count=0)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpConnection.readResponse(HttpConnection.java:211)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpTransport.readResponseHeaders(HttpTransport.java:80)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:917)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpEngine.access$300(HttpEngine.java:95)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpEngine$NetworkInterceptorChain.proceed(HttpEngine.java:902)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:760)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.Call.getResponse(Call.java:274)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.Call$ApplicationInterceptorChain.proceed(Call.java:230)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.Call.getResponseWithInterceptorChain(Call.java:201)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.Call.execute(Call.java:81)
08-06 00:29:47.445 23838 23852 E hoge : at jp.co.crowdworks.flavorsample.ApplicationTest.upload(ApplicationTest.java:113)
08-06 00:29:47.445 23838 23852 E hoge : at jp.co.crowdworks.flavorsample.ApplicationTest.test2UploadScreenshotFile(ApplicationTest.java:100)
08-06 00:29:47.445 23838 23852 E hoge : at java.lang.reflect.Method.invokeNative(Native Method)
08-06 00:29:47.445 23838 23852 E hoge : at java.lang.reflect.Method.invoke(Method.java:515)
08-06 00:29:47.445 23838 23852 E hoge : at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
08-06 00:29:47.445 23838 23852 E hoge : at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestCase.runBare(TestCase.java:134)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestResult$1.protect(TestResult.java:115)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestResult.runProtected(TestResult.java:133)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.DelegatingTestResult.runProtected(DelegatingTestResult.java:90)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestResult.run(TestResult.java:118)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.AndroidTestResult.run(AndroidTestResult.java:49)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestCase.run(TestCase.java:124)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.NonLeakyTestSuite$NonLeakyTest.run(NonLeakyTestSuite.java:63)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestSuite.runTest(TestSuite.java:243)
08-06 00:29:47.445 23838 23852 E hoge : at junit.framework.TestSuite.run(TestSuite.java:238)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.DelegatingTestSuite.run(DelegatingTestSuite.java:103)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.AndroidTestSuite.run(AndroidTestSuite.java:63)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.junit3.JUnit38ClassRunner.run(JUnit38ClassRunner.java:90)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.Suite.runChild(Suite.java:128)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.Suite.runChild(Suite.java:27)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
08-06 00:29:47.445 23838 23852 E hoge : at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:54)
08-06 00:29:47.445 23838 23852 E hoge : at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:228)
08-06 00:29:47.445 23838 23852 E hoge : at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1729)
08-06 00:29:47.445 23838 23852 E hoge : Caused by: java.io.EOFException: \n not found: size=0 content=...
08-06 00:29:47.445 23838 23852 E hoge : at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:200)
08-06 00:29:47.445 23838 23852 E hoge : at com.squareup.okhttp.internal.http.HttpConnection.readResponse(HttpConnection.java:191)
08-06 00:29:47.445 23838 23852 E hoge : ... 41 more
UnexpectedなEOSと。
ぐぐってみると、こんな記事が引っかかりました。
http://d.hatena.ne.jp/knight_9999/20141119/1416365367
connection: close
ヘッダを付けてやれば治る。(もしくは、http.keepAlive=falseにすれば確実に治る?)と。
ということで
private void upload(File file){
final OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder()
.url("https://api-content.dropbox.com/1/files_put/auto/"+ Build.MODEL+"_"+file.getName())
.addHeader("Authorization", "Bearer XXXXXXXXXXXXXX(DropboxのAPIトークン)XXXXXXXXXXXXXX"))
.addHeader("connection", "close") //★追加
.post(RequestBody.create(MediaType.parse("image/png"), file))
.build();
とりあえずconnection:closeを追加してみました。
すると・・・
キタ━━━━(゚∀゚)━━━━!!
となりました!
∑(゚Д゚)ガーン
これはいろいろと大変そうだ。
P.S.
いちおう、AWSのフォーラムにも類似のissue報告はあげてみましたが、8/26現在、報告はないですね・・・。
https://forums.aws.amazon.com/thread.jspa?messageID=667834򣂺