愛碼哥是國內領先的移動應用快速開發平臺。開發者稍作學習,就可以用xml + JavaScript定義原生的移動應用,同時生成iOS和Android上的app。由於這種開發方式的靈活快速,愛碼哥在國內已經有了龐大的用戶群體。(瞭解更多愛碼哥的方案可訪問官網https://www.imagjs.com/)
應用是有了,很多用戶接下來面臨新的挑戰,就是針對眾多不同系統版本、不同尺寸和分辨率的手機,如何驗證移動應用在這些機型上都適配,例如在不同Android版本上是否所有功能都能正常使用,不會出現崩潰和未知錯誤。在不同的界面上顯示是否正常。
如果這個兼容性驗證工作讓人來做,眾多的手機一一測過來,耗時長,效率低,是不現實的。這個工作非自動化測試莫屬。
為更好的幫助移動應用開發者提高應用的質量和兼容性。愛碼哥聯合聆播科技,推出了快速易學的愛碼哥應用自動化解決方案。作為自動化測試的專家,聆播科技有多款自動化測試產品,用戶可以使用JavaScript / Node.JS 開發針對移動應用、Web、API等類型的自動化腳本。愛碼哥的用戶只要掌握JavaScript,就可以開發從應用到自動化測試腳本全套內容。成為真正的全棧工程師。
這裡提供的愛碼哥應用測試方案使用CukeTest作為測試工具,它可以安裝在多種平臺上(包括Windows、Mac),能開發行為驅動的自動化腳本。編寫iOS應用自動化測試的腳本需要在Mac平臺上,而Android自動化測試的腳本根據用戶習慣在任何一個平臺上都可以。
下面就針對愛碼哥的樣例應用,給大家演示如何做Android的自動化測試。
先看下被測應用,這個是應用的首頁:
下面是登陸頁:
1. 環境準備
1. 下載CukeTest,下載地址:http://cuketest.com/, Windows版或Mac版都可以。
2. 自備一臺Android設備,下載愛碼哥app並安裝。
3. PC端配置好Androd開發環境。
2. 設計場景
1. 打開CukeTest,創建一個空的項目。文件——新建項目——項目類型選擇 Basic。
2. 創建成功之後,在feature文件中編輯場景,即測試用例的文字描述。這是行為驅動開發(BDD)的特點。BDD以場景為中心,驅動自動化測試腳本。既增強了可讀性,易於腳本的維護和團隊的溝通,也方便開發者整理測試腳本的思路。瞭解更多關於BDD的概念可以參考“為什麼要行為驅動開發(BDD)”(http://cuketest.com/zh-cn/bdd/why_bdd.html)
剛創建項目默認視圖為:
針對我們的app,我們要編寫一個下圖的簡單操作場景:
我們也可以將CukeTest切換到文本界面,直接將如下代碼複製進去。
# language: zh-CN
功能: app 功能驗證
驗證app 基本功能:
底部導航跳轉
場景: 首頁分類模塊-子模塊驗證
假如切換到首頁
當手機滑動到分類模塊
那麼此模版應該有12個子模塊
場景: 用戶登錄場景
假如切換到首頁
當點擊登錄/註冊按鈕
那麼應該跳轉到登錄頁面
同時手機號碼輸入"170****1097"
同時密碼欄輸入"123456"
當點擊登錄按鈕
那麼用戶應該成功登錄,並且首頁顯示手機號碼"170****1097"
場景: 底部導航欄UI驗證
假如切換到首頁
那麼底部導航欄應該有3個模板
那麼底部導航欄的文字分別是 | 愛碼哥 | 文檔 | 我的 |
同時獲取到底部所有導航欄並遍歷點擊
場景: 用戶退出登錄
假如切換到我的模塊
當點擊退出登錄按鈕
那麼彈出確認退出提示,點擊確認按鈕
那麼應該用戶退出登錄
3. 安裝項目依賴
我們使用其它語言來寫手機自動化代碼的時候,需要用到一些第三方的依賴庫。同樣,我們在這個項目中也需要使用到第三方的庫文件。
此次我們用的依賴庫如下:
- webdriverio: 封裝的web和手機端的自動化操作控件庫
- @types/webdriverio:幫助我們在CukeTest中寫代碼時能夠獲得智能提示的庫
項目根目錄下執行 npm install webdriverio @types/webdriverio --save 即可。安裝了這個包之後CukeTest會在你開發的時候自動提供這個庫相關的智能提示。
4. 生成自動化代碼樣例
打開features/step_definitions/下的definitions1.js文件,逐一點擊我們feature文件中步驟後面的灰色按鈕,CukeTest會自動為我們生成自動化樣例代碼到definitions1.js文件中:
生成完成後,CukeTest會根據代碼塊自動生成對應的代碼示例。
5.利用appium 進行元素定位
1. 打開 appium 桌面版,點擊start server啟動appium server:
2. File – New Session Window打開新的窗口,配置app啟動參數,根據自己的手機配置。
主要參數說明
deviceName:手機序列串號
appPackage:被測應用的包名
appActivity:被測應用的入口
resetKeyboard:是否重置手機鍵盤
noReset:不要重新安裝手機app
將手機與PC端通過數據線連接,點擊【Start Session】 啟動appium連接手機,此時appium 應該顯示手機客戶端打開的界面。
根據appium桌面端提供的ui元素追蹤器,我們可以很方便的定位到每個元素。通過webdriverio(http://webdriver.io/api.html) 庫提供的API,即可完成對app的自動化。
6. 代碼部分
- 設置驅動, 新建features/support/android_driver.js
var webdriverio = require('webdriverio')
function createDriver() {
let options = {
desiredCapabilities: {
platformName: 'Android',
platformVersion: '5.1',
automationName: 'UiAutomator2',
deviceName: "Y15QKCPH278J4",
appPackage: "com.imagjs.android.mh20170602",
appActivity: "com.imagjs.imag.ImagActivity",
noReset: true,
unicodeKeyboard: true
},
host:"127.0.0.1",
port:4723
};
let client = webdriverio.remote(options);
return client;}exports.driver = createDriver();
2. 設置運行時,新建features/suport/hooks.js
const { After, BeforeAll, Before, AfterAll, setDefaultTimeout } = require('cucumber');
const { driver } = require('./android_driver');
let cuketest = require('cuketest');
// 設置默認的超時時間
setDefaultTimeout(60 * 1000);
BeforeAll(async function () {
//所有場景運行之前
await driver.init();
await cuketest.delay(5000);
return driver.timeoutsImplicitWait(5*1000)})
Before(function () {
//定義場景運行之前
})
After(async function () {
//定義場景運行之後
let screenshot = await driver.saveScreenshot();
this.attach(screenshot, 'image/png');
});
AfterAll(function () {
//perform some shared teardown
return driver.end();
})
3. 實現場景代碼, 編輯 features/step_definitions/definitions1.js
var { Given, When, Then } = require('cucumber')
let { driver } = require('../support/android_driver')
let cuketest = require('cuketest')
let assert = require('assert');
async function getBottomList() {
let bottomList = driver.element('android=new UiSelector().resourceId("com.imagjs.android.mh20170602:id/main_bottom_tablayout").index(1)')
let list = await bottomList.elements('android=new UiSelector().className("android.widget.LinearLayout")')
return list;
}
Given(/^底部導航欄應該有(\d+)個模板$/, async function (num) {
let list = await getBottomList();
return assert.equal(list.value.length, num, "數值不相等")
});
Given(/^底部導航欄的文字分別是$/, async function (table) {
let text = await driver.getText('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView')
console.log("text", text)
let asserVal = table.raw()[0]
for (let i = 0; i < text.length; i++) {
assert.equal(text[i], asserVal[i]);
}
});
Then(/^切換到首頁$/, async function () {
await driver.click('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.LinearLayout[1]')});Given(/^獲取到底部所有導航欄並遍歷點擊$/, async function () {
let list = await getBottomList();
for (let i = 1; i < list.value.length + 1; i++) {
await driver.click('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.LinearLayout[' + i + ']');
await cuketest.delay(1000)
}
});
When(/^手機滑動到分類模塊$/, async function () {
await driver.swipe('android=new UiSelector().resourceId("com.imagjs.android.mh20170602:id/contents_view_pager")', 50, -500, 1000);
});
Then(/^此模版應該有(\d+)個子模塊$/, async function (num) {
let eles = await driver.elements('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.support.v4.view.ViewPager/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout[3]/android.support.v7.widget.RecyclerView/android.widget.LinearLayout')
return assert(eles.value.length, num, "子模塊數量不一致");
});
When(/^點擊登錄\/註冊按鈕$/, async function () {
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.support.v4.view.ViewPager/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.RelativeLayout/android.widget.LinearLayout[1]/android.widget.TextView'
await driver.click(selector)
});
Then(/^應該跳轉到登錄頁面$/, async function () {
await cuketest.delay(1000);
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.RelativeLayout[2]/android.widget.LinearLayout/android.widget.TextView'
let text = await driver.getText(selector)
return assert.equal(text,"www.imagjs.com")
});
Then(/^手機號碼輸入"([^"]*)"$/, async function (phone) {
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[1]/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[2]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.EditText'
await driver.clearElement(selector)
await driver.element(selector).setValue(phone);
});
Then(/^密碼欄輸入"([^"]*)"$/, async function (passwd) {
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[3]/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[2]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.EditText'
await driver.clearElement(selector);
await driver.element(selector).setValue(passwd)
});
When(/^點擊登錄按鈕$/, async function () {
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[5]/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.RelativeLayout[1]/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.Button'
await driver.click(selector)
});
Then(/^用戶應該成功登錄,並且首頁顯示手機號碼"([^"]*)"$/, async function (phone) {
await cuketest.delay(2000);
let phoneSelector='/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.support.v4.view.ViewPager/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.RelativeLayout/android.widget.LinearLayout[1]/android.widget.TextView'
let text = await driver.getText(phoneSelector);
return assert.equal(text,phone)
});
Given(/^切換到我的模塊$/, async function () {
let selector='/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.widget.LinearLayout/android.widget.LinearLayout[3]'
await driver.click(selector);
});
When(/^點擊退出登錄按鈕$/, async function () {
let selector = '/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.support.v4.view.ViewPager/android.widget.LinearLayout/android.support.v4.view.ViewPager/android.view.View/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout[4]/android.widget.LinearLayout[3]'
await driver.click(selector);
});
When(/^彈出確認退出提示,點擊確認按鈕$/, async function () {
await cuketest.delay(2000);
await driver.alertAccept();
});
Then(/^應該用戶退出登錄$/, async function () {
let selector='/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[1]/android.widget.RelativeLayout/android.support.v4.view.ViewPager/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.RelativeLayout/android.widget.LinearLayout[1]/android.widget.TextView'
let text = await driver.getText(selector)
return assert.equal(text,"登錄/註冊");
});
JavaScript語法,愛碼哥的用戶有沒有感到很親切?
7. 運行
先將appium啟動, 再點擊【運行項目】,可以看到手機會自動按照我們設計的場景運行起來。
運行完成後自動生成測試報告。
8.總結
這裡使用了Node.js + Cucumber 開發了Appium的客戶端腳本,使用CukeTest開發環境,優勢:
- 完全的開源的代碼,可以在多平臺運行,包括Windows, Linux, Mac等。
- CukeTest提供的開發環境快速生成自然語言的測試用例和框架代碼,及可視化報表、視頻等。
- 最新的Node.js和JavaScript的異步語法async/await在腳本開發中得到了廣泛的應用,再也不用為異步編程的難度而煩惱了。
原文鏈接:https://mp.weixin.qq.com/s/7QIwd7CfG_ZQSUByYT6Lag
閱讀更多 愛碼哥 的文章