透過 Visual Studio 2017 與 Appium 測試安卓手機上的 App

by Ouch Liu (劉耀群) — on  ,  , 

cover-image

前言

在先前的 透過 Visual Studio 2017 與 Appium 測試安卓手機上的行動版網頁 一文中跟大家介紹了使用 Appium 來達到 Mobile Web 測試的自動化。

不過,在 Mobile 的世界裡, App 才是大家更想關心的事。所以,這次就來跟大家分享使用 Appium 來作到 Moble App 的測試自動化囉!!

在接下去之前,讓我們再來看一次 Appium 的運作架構。 Appium 的運作架構

環境準備

在我們動手開始寫程式之前,得要先確認下列項目都已經安裝/設定好:

  • 安裝 Visual Studio 2017 的行動裝置相關開發功能(這樣才能使用 VS2017 附的安卓模擬器)

  • 安裝 Appium

  • 安裝 Java SDK

  • 在 Windows 的系統變數中加入 ANDROID_HOME,並且將它指到 Android SDK 的安裝路徑(預設為 C:\Program Files (x86)\Android\android-sdk)

  • 在 Windows 的系統變數中加入 JAVA_HOME ,並且並將它指定到 Java SDK 的 bin 資料夾 設定 JAVA_HOME 路徑

  • 在 Windows 系統變數中的 Path 項目中加上以下幾個路徑:

    • %JAVA_HOME%
    • %ANDROID_HOME%
    • %ANDROID_HOME%\tools\
    • %ANDROID_HOME%\platform-tools\ 設定 Path 中的路徑

Android 模擬器設定

透過 VS 開發最大的好處就是可以在 VS 裡面管理所有相關的功能,包含 Android SDK 和模擬器。 安裝好 VS 之後,不妨先透過 Android SDK Manager (Tools -> Android -> Android SDK Manager…)來更新 Android SDK 到最新的版本。

另外,如果你使用 Intel 的 CPU 的話,也建議針對 Android 4.4.2 安裝 Google APIs Intel x86 Atom System Image,並且安裝 Intel® Hardware Accelerated Execution Manager 以獲得更好的效能。

安裝Google APIs Intel x86 Atom System Image

若有啟用 Hyper-V 的話,HAXM 可能會和它相衝,這時候可以透過在命令提示字元輸入 bcdedit /set hypervisorlaunchtype off 之後重新開機以關閉 Hyper-V 。

SDK 更新完之後,就可以透過 Tools -> Android -> Android Emulator Manager… 來管理虛擬機囉。在這邊,我選擇修改原來內建的 AVD_GalaxyNexus_ToolsForApacheCordova,並且調整 CPU/ABI 為 Google APIs Intel Atom (x86)、Skin 為 Skin with dynamic hardware controls、勾選 Use Host GPU

編輯安卓模擬器設定

設定完成之後,就可以按下 Android Virtual Device (AVD) Manager 裡面的啟動按鈕來開啟模擬器啦~

按下Start鈕啟動模擬器

如果看到模擬器正常執行,就可以開始動手寫測試程式囉。

若要在實機上進行測試的話,可能得先自行進行開發者解鎖。

撰寫自動化測試程式碼

在撰寫自動化測試的程式碼之前,首先我們得要找一個 App 來測試(廢話)。

基於自己的程式自己寫的原則,我這邊就直接拿 [透過 Gorilla Player 在裝置上即時預覽 Xamarin Forms 介面] 一文中的範例稍作修改來供測試使用。

這個範例程式的內容很簡單,基本上就是用一個 ListView 去呈現書籍的資料,點擊之後會切到詳細資料頁面這樣而已。

而我希望能透過 Appium 來幫我儘可能的點選 ListView 中的每個項目,並且確認每個項目的資料值都如我預期,以及詳細資料頁裡面的資料也符合預期這樣。(不過我的測試並沒有針對圖片去作任何的驗證。)

所以我寫出來的程式碼如下:

UnitTest1.cs

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using MobileAppUnitTestWithAppium.Models;
using Newtonsoft.Json;
using NUnit.Framework;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Appium.Enums;
using OpenQA.Selenium.Remote;

namespace UnitTestProject
{
    [TestFixture]
    public class UnitTest1
    {
        //宣告 Appium Driver,並指定使用 Android Element
        AppiumDriver<AndroidElement> _driver;

        [Test]
        public void TestMobileApp()
        {
            DesiredCapabilities desiredCapabilities = new DesiredCapabilities();

            //指定平台為安卓
            desiredCapabilities.SetCapability( MobileCapabilityType.PlatformName , MobilePlatform.Android );

            //指定裝置名稱,裝置名稱可以透過在 Tools -> Android Adb Command Prompt... 中輸入 adb devices -l 取得
            desiredCapabilities.SetCapability( MobileCapabilityType.DeviceName , "kate" );

            //指定要測試的 App,基本上就是 Android 專案的名稱
            desiredCapabilities.SetCapability( "appPackage" , "MobileAppUnitTestWithAppium.Android" );

            //指定 App 的 MainActivity,這個值可以在 Android 專案下的 obj\Debug\android\AndroidManifest.xml 檔裡面找到
            desiredCapabilities.SetCapability( "appActivity" , "md566d58bce9157a88432d9c294e8892f90.MainActivity" );

            //建立 AppiumDriver 的 Instance ,並指定 Appium Server 的路徑
            _driver = new AndroidDriver<AndroidElement>( new Uri( "http://127.0.0.1:4723/wd/hub" ) , desiredCapabilities );

            AndroidElement listView = _driver.FindElementById( "listView" );

            var listViewItems = listView.FindElementsByClassName( "android.widget.LinearLayout" );

            IEnumerable<Book> expectedBooks = null;

            var directory = Path.GetDirectoryName( System.Reflection.Assembly.GetExecutingAssembly().Location );

            using( StreamReader file = File.OpenText( $"{directory}\\Books.json" ) )
            {
                string jsonString = file.ReadToEnd();
                expectedBooks = JsonConvert.DeserializeObject<IEnumerable<Book>>( jsonString );
            }

            Size size = _driver.Manage().Window.Size;

            //抓取螢幕高度的中心點
            int screenHeightStart = (int) (size.Height * 0.5);

            foreach(var listViewItem in listViewItems )
            {
                //取得目前抓到的項目
                Book actualBook = new Book
                {
                    Name = listViewItem.FindElementById( "txtName" ).Text ,
                    Author = listViewItem.FindElementById( "txtAuthor" ).Text ,
                    Price = Double.Parse( listViewItem.FindElementById( "txtPrice" ).Text )
                };

                Book expectedBook = expectedBooks.First( b => b.Name == actualBook.Name );

                Assert.AreEqual( expectedBook.Name , actualBook.Name );

                Assert.AreEqual( expectedBook.Author , actualBook.Author );

                Assert.AreEqual( expectedBook.Price , actualBook.Price );

                listViewItem.Click();

                actualBook = new Book
                {
                    Name = _driver.FindElementById( "txtName" ).Text ,
                    Author = _driver.FindElementById( "txtAuthor" ).Text ,
                    Isbn = _driver.FindElementById( "txtIsbn" ).Text ,
                    Price = Double.Parse( _driver.FindElementById( "txtPrice" ).Text ) ,
                    ReleaseDate = DateTime.Parse( _driver.FindElementById( "txtReleaseDate" ).Text ) ,
                };

                Assert.AreEqual( expectedBook.Name , actualBook.Name );

                Assert.AreEqual( expectedBook.Author , actualBook.Author );

                Assert.AreEqual( expectedBook.Isbn , actualBook.Isbn );

                Assert.AreEqual( expectedBook.ReleaseDate , actualBook.ReleaseDate );

                Assert.AreEqual( expectedBook.Price , actualBook.Price );

                //找到返回上一頁的按鈕
                AndroidElement backButton = _driver.FindElementByClassName( "android.widget.ImageButton" );

                //點選返回上一頁的按鈕
                backButton.Click();

                //每次都把 ListView 往上滑一點點
                _driver.Swipe( 0 , screenHeightStart , 0 , screenHeightStart - listViewItem.Size.Height / 2 , 1000 );
            }
        }

        [TearDown]
        public void TearDown()
        {
            _driver?.CloseApp();
        }

    }
}

在進行測試之前,請先將 App 佈署到要測試的裝置上,並且打開桌上面的 Appium 圖示來啟動 Appium Server。 基本上,完全不需要更改任何設定,只需要按下 Start Server 1.6.4 按鈕就行了。

按下 Start Server 1.6.4 按鈕

Appium Server 啟動之後的畫面如下:

Appium Server 啟動後的畫面

在開始執行測試之前,請務必先確認 Appium Server 已經正常啟動,而且 Android 模擬器也能正常運作。

眼尖的朋友應該會發現,我在描述測試案例的句子裡面有語病,我說儘可能的點選 ListView 中的每個項目,這是因為 Appium 目前並沒有內建 ScrollToEnd 之類的 API,之前本來還有 ScrollTo 和 ScrollToExact 兩個 API 可以用,但是後來都被拔掉了,所以我只能儘可能的做到測好測滿。

以安卓為例,透過 Appium 來抓取 ListView 中的子控制項的時候,並不像網頁的 DOM物件那樣只要元件樹載入完畢,就能抓到所有項目;反之,我們能抓取到的項目個數會受限於螢幕大小(也就是可視範圍能顯示的數目),並沒有辦法一次抓到所有的項目。

如果你有一個超級長的 ListView ,而且要針對裡面的每個元素進行測試的話,可能就得自己想辦法搭配捲動畫面的功能,先把 ListView 的所有項目都先爬出來,再逐一進行測試。

另外,如果要透過控制項的 ID 來抓取控制項的話,在不同的平台上會有不同的屬性需要特別設定,例如以 Xamarin.Forms 來說,就得透過 AutomationId 來作為測試時期控制項唯一的識別 Id。甚至有些平台/框架的控制項 Id 會是平台/框架主動賦予的,在使用上就得格外注意。

在撰寫測試程式的時候,也可以透過 Android SDK 中的 UI Automator Viewer 來了解 App 中控制項的名稱和位置等等關係。

UI Automator Viewer

可以透過 C:\Program Files (x86)\Android\android-sdk\tools\uiautomatorviewer.bat 來啟動 UI Automator Viewer。

以上,希望能幫到有相關需求的朋友們。

最後,附上專案的原始碼,請自行取用:

Sample

Comments