iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
0
Mobile Development

Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 系列 第 8

Day8 Espresso

今天會講昨天有用到但還沒講過的espresso

espresso簡而言之就是一個ui test用的library
通常會跟UI automator混搭
因為espresso只能測app本身 不能跨app
例如你想要模擬按下手機home鍵的行為 espresso是做不到的
但ui automator可以

今天會拿這份codelab當基礎來講espresso
https://codelabs.developers.google.com/codelabs/android-training-espresso-for-ui-testing/index.html#0

https://github.com/google-developer-training/android-fundamentals-apps-v2
請直接下載這份code

然後在android studio開啟TwoActivities這份專案
然後對app點右鍵 ->Open Module Setting -> 選擇build Tools Version

https://ithelp.ithome.com.tw/upload/images/20190923/201202799k2dDDfnwd.png
https://ithelp.ithome.com.tw/upload/images/20190923/20120279DxZKizU2nK.png

然後運行
https://i.imgur.com/9MRuZhX.gif
功能是 A Activity傳訊息給 B Activity
然後B Activity 傳訊息回去給A Activity

另外 如果你要導入espresso到現有專案的話
請確保有dependencies相關libary

附上範例的build.gradle

android {
   defaultConfig {
     ...
       testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
   }
}

dependencies {
  testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 
           'com.android.support.test.espresso:espresso-core:3.0.1'
}

接著先找到ExampleInstrumentedTest 重新命名為ActivityInputOutputTest
shift+F6 或右鍵Refactor -> Rename

接著再新增第一個@test之前 先增加一個@Rule

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

現在這個class 有幾個註釋
@RunWith: 運行的測試類別 請加在class開頭
通常都是用 @RunWith(AndroidJUnit4.class)

@Rule:
ActivityTestRule 這個rule是用來測試單個Activity的,Activity將在@Test和@Before前啟動
在此基礎下可運行
ViewMachers:找View
ViewActions: 執行指定行為
ViewAssertions:驗證測試結果

@Test:
告訴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.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

@Test
public void activityLaunch() {
  //首頁的button_main運行click
   onView(withId(R.id.button_main)).perform(click());
//此時會跳轉到第二頁 驗證是否有第二頁的元件
   onView(withId(R.id.button_second)).check(matches(isDisplayed()));
//點選第二頁元件 跳轉回第一頁
   onView(withId(R.id.button_second)).perform(click());
//確認是否有跳轉成功 如果有 應該能找到首頁的元件
   onView(withId(R.id.button_main)).check(matches(isDisplayed()));
}

接著測試輸入框

import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

@Test
public void textInputOutput() {
//輸入訊息
   onView(withId(R.id.editText_main)).perform(typeText("This is a test."));
   onView(withId(R.id.button_main)).perform(click());
//第二頁元件 確認數據是否符合預期
   onView(withId(R.id.text_message)).check(matches(withText("This is a test.")));
}

完整範例
https://github.com/google-developer-training/android-fundamentals-apps-v2/blob/master/TwoActivitiesEspresso/app/src/androidTest/java/com/example/android/twoactivities/ActivityInputOutputTest.java

然後espresso有一個錄製腳本的功能
這次拿另一個專案當範例
找到 Scorekeeper 專案後
在android studio打開他

運行測試功能是否正常
https://i.imgur.com/vevZxyf.gif
確認可以運行後
先停止運行app
接著點選IDE上方的run->Record Espresso Test
![https://ithelp.ithome.com.tw/upload/images/20190923/20120279brpjdaVfc1.png]

步驟1: 打開模擬器後點擊Team 1的 +
會變成下圖
https://ithelp.ithome.com.tw/upload/images/20190923/201202792wDStkPdFN.png

步驟二:點選
Add Assertion
之後視窗右邊會出現畫面預覽
步驟三:點選1 此時畫面應該會跟下圖一致
https://ithelp.ithome.com.tw/upload/images/20190923/20120279EXXupKZjSK.png
步驟四:接著點選
"Save Assertion"

重複上敘步驟將增減行為都新增上去
最後按ok新增 然後記得改class名稱 讓別人容易理解這個測試的測試項目是什麼
ex:ScorePlusMinusTest

然後再次運行測試項目應該會順利通過

代碼:
ScorePlusMinusTest.kt

import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.matcher.ViewMatchers.*
import android.support.test.filters.LargeTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import android.view.View
import android.view.ViewGroup
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.allOf
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.IsInstanceOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(AndroidJUnit4::class)
class ScorePlusMinusTest {

   @Rule
   @JvmField
   var mActivityTestRule = ActivityTestRule(MainActivity::class.java)

   @Test
   fun scorePlusMinusTest() {
       val appCompatImageButton = onView(
               allOf(withId(R.id.increaseTeam1), withContentDescription("Plus Button"),
                       childAtPosition(
                               childAtPosition(
                                       withClassName(`is`("android.widget.LinearLayout")),
                                       0),
                               3),
                       isDisplayed()))
       appCompatImageButton.perform(click())

       val textView = onView(
               allOf(withId(R.id.score_1), withText("1"),
                       childAtPosition(
                               childAtPosition(
                                       IsInstanceOf.instanceOf(android.widget.LinearLayout::class.java),
                                       0),
                               2),
                       isDisplayed()))
       textView.check(matches(withText("1")))

       val appCompatImageButton2 = onView(
               allOf(withId(R.id.decreaseTeam1), withContentDescription("Minus Button"),
                       childAtPosition(
                               childAtPosition(
                                       withClassName(`is`("android.widget.LinearLayout")),
                                       0),
                               1),
                       isDisplayed()))
       appCompatImageButton2.perform(click())

       val textView2 = onView(
               allOf(withId(R.id.score_1), withText("0"),
                       childAtPosition(
                               childAtPosition(
                                       IsInstanceOf.instanceOf(android.widget.LinearLayout::class.java),
                                       0),
                               2),
                       isDisplayed()))
       textView2.check(matches(withText("0")))

       val appCompatImageButton3 = onView(
               allOf(withId(R.id.increaseTeam2), withContentDescription("Plus Button"),
                       childAtPosition(
                               childAtPosition(
                                       withClassName(`is`("android.widget.LinearLayout")),
                                       1),
                               3),
                       isDisplayed()))
       appCompatImageButton3.perform(click())

       val textView3 = onView(
               allOf(withId(R.id.score_2), withText("1"),
                       childAtPosition(
                               childAtPosition(
                                       IsInstanceOf.instanceOf(android.widget.LinearLayout::class.java),
                                       1),
                               2),
                       isDisplayed()))
       textView3.check(matches(withText("1")))

       val appCompatImageButton4 = onView(
               allOf(withId(R.id.decreaseTeam2), withContentDescription("Minus Button"),
                       childAtPosition(
                               childAtPosition(
                                       withClassName(`is`("android.widget.LinearLayout")),
                                       1),
                               1),
                       isDisplayed()))
       appCompatImageButton4.perform(click())

       val textView4 = onView(
               allOf(withId(R.id.score_2), withText("0"),
                       childAtPosition(
                               childAtPosition(
                                       IsInstanceOf.instanceOf(android.widget.LinearLayout::class.java),
                                       1),
                               2),
                       isDisplayed()))
       textView4.check(matches(withText("0")))

       val textView5 = onView(
               allOf(withId(R.id.score_2), withText("0"),
                       childAtPosition(
                               childAtPosition(
                                       IsInstanceOf.instanceOf(android.widget.LinearLayout::class.java),
                                       1),
                               2),
                       isDisplayed()))
       textView5.check(matches(withText("0")))
   }

   private fun childAtPosition(
           parentMatcher: Matcher<View>, position: Int): Matcher<View> {

       return object : TypeSafeMatcher<View>() {
           override fun describeTo(description: Description) {
               description.appendText("Child at position $position in parent ")
               parentMatcher.describeTo(description)
           }

           public override fun matchesSafely(view: View): Boolean {
               val parent = view.parent
               return parent is ViewGroup && parentMatcher.matches(parent)
                       && view == parent.getChildAt(position)
           }
       }
   }
}

不過老實說我覺得錄製的功能不太好用就是了...

solution
https://github.com/google-developer-training/android-fundamentals-apps-v2/tree/master/ScorekeeperEspresso


上一篇
Day7 MVVM專案-1
下一篇
Day9 MVVM專案-1a
系列文
Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言