GUIを用いたアプリのテストで、テストの開始前にアニメーションを無効化することが定石となっています。理由は「テストの安定性を確保するためである」と、ドキュメントに記載されています。
この無効化の処理を自動で行えたら便利です。なので、アニメーションを無効にするテストルールを作成してみました。
アニメーションスケール
Android端末のDeveloper Optionsを有効にすると、アニメーションスケールという項目が変更可能になります。
スケールとはアニメーションが変化するスピードを表しています。デフォルトは1.0×です。2.0×は2倍の時間をかけてアニメーションが変化し、0.0×はアニメーションが即座に終了します。0.0×はアニメーションをしないので無効であることを表しています。
「テストの開始前にアニメーションを無効化する」とは次に示す3項目を無効にすることです。
Developer Options | XXX scaleをクリックすると |
---|---|
スケール | 影響範囲 | 具体例 |
---|---|---|
Window animation | ポップアップされるウィンドウの開閉時 | Toast |
Transition animation | コンポーネントの遷移時 | Activityの開閉 ホーム画面の表示 |
Animator duration | Viewのアニメーション | ObjectAnimator Snackbarの開閉 |
スケールを変更する方法
adb shellコマンドのsettingsツールを使って変更します。
アニメーションスケールはSettings.Globalに属しています。Settings.Globalに登録されているキーを指定してget/putすれば変更が可能です。
テストではUiDevice(UiAutomator)からadb shellコマンドが発行できるので、これを使います。
スケールの取得
fun getWindowAnimationScale(): Float = _GetScale(Settings.Global.WINDOW_ANIMATION_SCALE) fun getTransitionAnimationScale(): Float = _GetScale(Settings.Global.TRANSITION_ANIMATION_SCALE) fun getAnimatorDurationScale(): Float = _GetScale(Settings.Global.ANIMATOR_DURATION_SCALE) private fun _GetScale(name: String): Float { val _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ) val _Command = String.format("settings get global %s", name) val _Result = _UiDevice.executeShellCommand(_Command) if (!_Result.startsWith("null")) { val _Scale = java.lang.Float.valueOf(_Result) if (_Scale >= 0.0f) { return _Scale } } return 1.0f }
public float getWindowAnimationScale() throws IOException { return _GetScale(Settings.Global.WINDOW_ANIMATION_SCALE); } public float getTransitionAnimationScale() throws IOException { return _GetScale(Settings.Global.TRANSITION_ANIMATION_SCALE); } public float getAnimatorDurationScale() throws IOException { return _GetScale(Settings.Global.ANIMATOR_DURATION_SCALE); } private float _GetScale(String name) throws IOException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _Command = String.format("settings get global %s", name); String _Result = _UiDevice.executeShellCommand(_Command); if (! _Result.startsWith("null")) { float _Scale = Float.valueOf(_Result); if(_Scale >= 0.0f) { return _Scale; } } return 1.0f; }
AVD(Android Virtual Device)の作成直後はスケールの取得を行うと”null”を返すので対応が必要です。”null”を返しますが1.0×と同じ動作をするので、デフォルト:1.0×を返すようにしています。スケールの操作を行うと設定したスケール値が返るようになります。
スケールの設定
fun putWindowAnimationScale(scale: Float) { _PutScale(Settings.Global.WINDOW_ANIMATION_SCALE, scale) } fun putTransitionAnimationScale(scale: Float) { _PutScale(Settings.Global.TRANSITION_ANIMATION_SCALE, scale) } fun putAnimatorDurationScale(scale: Float) { _PutScale(Settings.Global.ANIMATOR_DURATION_SCALE, scale) } private fun _PutScale(name: String, scale: Float) { val _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ) val _Command = String.format("settings put global %s %f", name, scale) _UiDevice.executeShellCommand(_Command) }
public void putWindowAnimationScale(float scale) throws IOException { _PutScale(Settings.Global.WINDOW_ANIMATION_SCALE, scale); } public void putTransitionAnimationScale(float scale) throws IOException { _PutScale(Settings.Global.TRANSITION_ANIMATION_SCALE, scale); } public void putAnimatorDurationScale(float scale) throws IOException { _PutScale(Settings.Global.ANIMATOR_DURATION_SCALE, scale); } private void _PutScale(String name, float scale) throws IOException { UiDevice _UiDevice = UiDevice.getInstance( InstrumentationRegistry.getInstrumentation() ); String _Command = String.format("settings put global %s %f", name, scale); _UiDevice.executeShellCommand(_Command); }
ただ、この方法には注意点があります。
- API<24の場合、スケール値を反映させるためにAndroid端末のリブートが必要
(putした値がgetで読み出せるが反映されない)
テストルールの作成
「スケールを変更する方法」の注意点を考慮して、テストルールは次のような仕様にしました。
- adb shellコマンドのsettingsツールを使って変更する
- API≧24で動作
- テスト後にテスト前の状態へ戻す
@TargetApi(24) class AnimationTestRule : TestRule { private val currWinScale = getWindowAnimationScale() private val currTraScale = getTransitionAnimationScale() private val currDurScale = getAnimatorDurationScale() // ----- ルール本体 ------------------------------------------------------- override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { try { putWindowAnimationScale(0.0f) // 前処理 putTransitionAnimationScale(0.0f) putAnimatorDurationScale(0.0f) base.evaluate() // テストの実施 } finally { putWindowAnimationScale(currWinScale) // 後処理 putTransitionAnimationScale(currTraScale) putAnimatorDurationScale(currDurScale) } } } } ... }
@TargetApi(24) public class AnimationTestRule implements TestRule { private float mCurrWinScale = 1.0f; private float mCurrTraScale = 1.0f; private float mCurrDurScale = 1.0f; public AnimationTestRule_j() { try { mCurrWinScale = getWindowAnimationScale(); mCurrTraScale = getTransitionAnimationScale(); mCurrDurScale = getAnimatorDurationScale(); } catch (IOException e) { e.printStackTrace(); } } // ----- ルール本体 ------------------------------------------------------- @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { try { putWindowAnimationScale(0.0f); // 前処理 putTransitionAnimationScale(0.0f); putAnimatorDurationScale(0.0f); base.evaluate(); // テストの実施 } finally { putWindowAnimationScale(mCurrWinScale); // 後処理 putTransitionAnimationScale(mCurrTraScale); putAnimatorDurationScale(mCurrDurScale); } } }; } ... }
テストルールの使用方法
使用方法はその他のテストルールと変わりません。@Ruleを付けてプロパティ(Javaの場合はフィールド)を宣言するだけです。
テストメソッドの途中でもputWindowAnimationScale/putTransitionAnimationScame/putAnimatorDurationScaleを用いてスケールの変更が可能です。
class Sample_Test { @get:Rule var animationRule = AnimationTestRule() // ------------------------------------------------------------------------ @Test fun Snackbarの表示() { onView(withId(R.id.btnSnackBar)).perform(click()) onView(withText("スナックバーを開きました!")).check(matches(isDisplayed())) animationRule.putAnimatorDurationScale(5.0f) assertThat(animationRule.getAnimatorDurationScale()).isEqualTo(5.0f) ... } }
public class Sample_Test { @Rule public AnimationTestRule mAnimationRule = new AnimationTestRule(); // ------------------------------------------------------------------------ @Test public void Snackbarの表示() throws Exception { onView(withId(R.id.btnSnackBar)).perform(click()); onView(withText("スナックバーを開きました!")).check(matches(isDisplayed())); mAnimationRule.putAnimatorDurationScale(5.0f); assertThat(mAnimationRule.getAnimatorDurationScale()).isEqualTo(5.0f); ... } }