Macaca之测试用例(Node.JS版)

测试用例代码篇:

const path = require('path');
const wd = require('wd');

describe('MXD mobile DEMO1', function() {
  this.timeout(5 * 60 * 1000); // mocha 设置超时时间

  var driver = wd.promiseChainRemote({ // 初始化 webdriver
    host: 'localhost',
    port: 3456
  });

  before(function() { // iOS需要传 udid 和 bundleId
    return driver.init({
      platformName: 'iOS',
      udid: 'xxxxxxxx', //这里我都填写了正确的信息,考虑到隐私性,这里不写出来,实际没问题
      bundleId: 'xxxxxxxxx'
    });
  });

  it('#1 test', function() { 
    return driver
          .elementByNameIfExists('我')
          .elementByName('我')
          .click();
  });

  after(function() { // 所有测试用例的统一后置动作
    return driver
      .sleep(1000)
      .quit();
  });
});

官方demo的用例

       mobile-app-sample.test.js:

'use strict';

require('should');
const KEY_MAP = require('webdriver-keycode');

var platform = process.env.platform || 'iOS';
platform = platform.toLowerCase();

const pkg = require('../package');

/**
 * download app form npm
 *
 * or use online resource: https://npmcdn.com/ios-app-bootstrap@latest/build/ios-app-bootstrap.zip
 *
 * npm i ios-app-bootstrap --save-dev
 *
 * var opts = {
 *   app: path.join(__dirname, '..', 'node_modules', 'ios-app-bootstrap', 'build', 'ios-app-bootstrap.zip');
 * };
 */

// see: https://macacajs.github.io/desired-caps

var iOSOpts = {
  deviceName: 'iPhone 6s',//模拟器的名称,例如‘iPhone 6’或者‘Nexus 5x’
  platformName: 'iOS',    //当前用例运行的平台 {iOS/Android/Desktop}
  //autoAcceptAlerts: true, //自动接受所有的系统弹窗信息。默认是 false
  //autoDismissAlerts:true  //自动拒绝所有的系统弹窗信息。默认是 false
  //reuse: 3, //0: 清楚数据并重装app。1: (默认) 卸载并重装 app。2: 仅重装 app。3: 在测试结束后保持app状态
  //udid: '',    //测试设备的唯一设备ID
  //bundleId: 'xudafeng.ios-app-bootstrap', //应用的 Bundle ID,例如 com.apple.Maps
  app: 'https://npmcdn.com/ios-app-bootstrap@latest/build/ios-app-bootstrap.zip'   //.app 或者 .apk 文件的绝对地址或者远程地址,或者是包含上述文件格式的 Zip 文件
};

var androidOpts = {
  platformName: 'Android',
  autoAcceptAlerts: false,
  isWaitActivity: true,
  //reuse: 3, //0: 启动并安装app。{1 (默认):卸载并重装app。2: 仅重装 app。3: 在测试结束后保持app状态。} 
  //udid: '',
  //package: 'com.github.android_app_bootstrap', //Android app 的 package name
  //activity: 'com.github.android_app_bootstrap.activity.WelcomeActivity', //启动时的 Activity name
  app: 'https://npmcdn.com/android-app-bootstrap@latest/android_app_bootstrap/build/outputs/apk/android_app_bootstrap-debug.apk'
};

const isIOS = platform === 'ios';
const infoBoardXPath = isIOS ? '//*[@name="info"]' : '//*[@resource-id="com.github.android_app_bootstrap:id/info"]';
const webviewButtonXPath = isIOS ? '//*[@name="Webview"]' : '//*[@resource-id="android:id/tabs"]/android.widget.LinearLayout[2]';

const wd = require('macaca-wd');

// override custom wd
require('./wd-extend')(wd, isIOS);

describe('macaca mobile sample', function() {
  this.timeout(10 * 60 * 1000);

  const driver = wd.promiseChainRemote({
    host: 'localhost',
    port: 3456
  });

  driver.configureHttp({
    timeout: 600 * 1000
  });

  before(function() {
    return driver
      .init(isIOS ? iOSOpts : androidOpts)
      .sleep(10 * 1000);
  });

  after(function() {
    return driver
      .sleep(1000)
      .quit();
  });

  it('#1 should login success', function() {
    return driver
      /*
      .title()
      .then(data => {
        console.log(`current focus ${isIOS ? 'viewController' : 'activity'}: ${data}`);
      })
      */
      .getWindowSize()
      .then(size => {
        console.log(`current window size ${JSON.stringify(size)}`);
      })
      .appLogin('中文+Test+12345678', '111111');
  });

  it('#2 should display home', function() {
    return driver
      .source()
      .then(res => {
        console.log(res);
      })
      .takeScreenshot();
  });

  it('#3 should scroll tableview', function() {
    return driver
      .testGetProperty()
      .waitForElementByName('HOME')
      .click()
      .waitForElementByName('list')
      .click()
      .sleep(2000);
  });

  it('#4 should cover gestrure', function() {
    return driver
      .waitForElementByName('Alert')
      .click()
      .sleep(5000)
      .acceptAlert()
      .sleep(1000)
      .customback()
      .waitForElementByName('Gesture')
      .click()
      .sleep(5000)
      .then(() => {
        return driver
          .touch('tap', {
            x: 100,
            y: 100
          })
          .sleep(1000)
          .elementByXPath(infoBoardXPath)
          .text()
          .then(text => {
            JSON.stringify(text).should.containEql('singleTap');
          });
      })
      .then(() => {
        return driver
          .touch('press', {
            x: 100,
            y: 100,
            duration: 2
          })
          .sleep(1000);
      })
      .then(() => {
        return driver
          .waitForElementByXPath(infoBoardXPath)
          .touch('pinch', {
            scale: 2,      // only for iOS
            velocity: 1,   // only for iOS
            direction: 'in',// only for Android
            percent: 0.2,  // only for Android
            steps: 200     // only for Android
          })
          .sleep(1000);
      })
      /*
      // TODO Android rotate
      .then(() => {
        return driver
          .touch('rotate', {
          })
          .sleep(1000);
      })*/
      .customback()
      .then(() => {
        return driver
          .touch('drag', {
            fromX: 100,
            fromY: 600,
            toX: 100,
            toY: 100,
            duration: 3
          })
          .sleep(1000);
      })
      .sleep(1000);
  });

  it('#5 should go into webview', function() {
    return driver
      .customback()
      .sleep(3000)
      .elementByXPath(webviewButtonXPath)
      .click()
      .sleep(3000)
      .takeScreenshot()
      .changeToWebviewContext()
      .elementById('pushView')
      .click()
      .changeToWebviewContext()
      .waitForElementById('popView')
      .click()
      .sleep(5000)
      .takeScreenshot();
  });

  it('#6 should go into test', function() {
    return driver
      .changeToNativeContext()
      .waitForElementByName('Baidu')
      .click()
      .sleep(5000)
      .takeScreenshot();
  });

  it('#7 should works with web', function() {
    return driver
      .changeToWebviewContext()
      .title()
      .then(title => {
        console.log(`title: ${title}`);
      })
      .url()
      .then(url => {
        console.log(`url: ${url}`);
      })
      .refresh()
      .sleep(2000)
      .elementById('index-kw')
      .getProperty('name')
      .then(info => {
        console.log(`get web attribute name: ${JSON.stringify(info)}`);
      })
      .waitForElementById('index-kw')
      .sendKeys('中文+Macaca')
      .elementById('index-bn')
      .click()
      .sleep(5000)
      .source()
      .then(html => {
        html.should.containEql('Macaca');
      })
      .execute(`document.body.innerHTML = "

${pkg.name}

"
`) .sleep(3000) .takeScreenshot(); })
; it('#8 should logout success', function() { return driver .changeToNativeContext() .waitForElementByName('PERSONAL') .click() .sleep(1000) .takeScreenshot() .waitForElementByName('Logout') .click() .sleep(1000) .takeScreenshot(); }); });

测试案例解析:

1、 配置

var iOSOpts = {
    platformName: 'iOS',
    platformVersion: '9.3',         
    deviceName: 'iPhone 5s',
    app: '/Users/chenximing/workspace/ios/macaca-test2/macaca-test-sample/app/ios-app-bootstrap.zip'
};
/*
    platformName:平台名称
    platformVersion:iOS系统版本,框架好像没用到这个参数,所以这玩意不重要
    deviceName:设备名称
    app:被测app路径
*/

2、测试案例

var wd = require('webdriver-client')(iOSOpts);
describe('macaca mobile sample', function() {
    this.timeout(5 * 60 * 1000);
    var driver = wd.initPromiseChain();
    driver.configureHttp({
        timeout: 600000
    });

    before(function() {
        return driver
            .initDriver();
    });

    after(function() {
        return driver
            .sleep(1000)
            .quit();
    });

    it('#1 should login success', function() {
        return driver
            .login('12345678', '111111')
            .sleep(1000);
    });

...

});

       这里可以细分为:

       (1). driver初始化

var wd = require('webdriver-client')(iOSOpts);
......
var driver = wd.initPromiseChain();
driver.configureHttp({
    timeout: 600000
});

       webdriver-client是什么?

       上篇说到macaca是c-s模式的测试框架,client负责被案例端调用的API,server负责调起instruments以及控制其执行测试。webdriver-client就是上面说到的client端,提供控制操作的API,《Macaca的API文档》。

       (2). 测试框架

describe('macaca mobile sample', function() {
    this.timeout(5 * 60 * 1000);

    ......

    before(function() {
        return driver
            .initDriver();
    });

    after(function() {
        return driver
            .sleep(1000)
            .quit();
    });

    it('#1 should login success', function() {
        return driver
            .login('12345678', '111111')
            .sleep(1000);
    });

    ...

});

       在这里,Macaca使用一个第三方的测试框架Mocha,macaca-cli在run的时候加载该框架。

       describe、before、after、it等关键字均为Mocha提供,和传统XUnit框架功能类似(Mocha默认是BDD模式,而XUnit是TDD模式),想了解更多,见Mocha主页。

       (3). 测试案例

...

it('#1 should login success', function() {
    return driver
        .login('12345678', '111111')
        .sleep(1000);
});

...

       it部分就是测试案例。

代码解析:

登录应用

       找到这两个字段,输入用户名和密码,并点击登录按钮

driver
    .waitForElementByXPath('//UIATextField[1]')
    .sendKeys('loginName')
    .waitForElementByXPath('//UIASecureTextField[1]')
    .sendKeys('123456')
    .sleep(1000)
    .sendKeys('\n')
    .waitForElementByName('Login')
    .click();

测试相关

切换到百度 的Tab,搜索TesterHome

driver
    .contexts()
    .then(arr => {
      return driver
        .context(arr[0]);
      })
    .elementByName('Baidu')
    .click()
    .contexts()
    .then(arr => {
      return driver
        .context(arr[arr.length - 1]);
      })
    .elementById('index-kw')
    .sendKeys('TesterHome')
    .elementById('index-bn')
    .tap()

退出应用

driver
    .contexts()
    .then(arr => {
      return driver
        .context(arr[0]);
      })
    .elementByName('PERSONAL')
    .click()

       需要注意的是,大部分的 Webdriver Client 实现都是基于 Promise 的,所有操作的结果都是异步的,并不是常规的将结果作为返回值的函数,如果想要得到某个步骤的返回值输出,需要在结尾跟上 then 去解析结果。

 > driver
     .currentContext()
     .then(console.log.bind(console))
   // NATIVE_APP

> driver
    .elementByName('Baidu')
    .then(console.log.bind(console))
  // PromiseElement {value: 7, browser: PromiseWebdriver}

       当然,为了安全起见,最好在 then 的第二个参数也加上一个函数来捕捉可能出现的错误信息,比如当我查询一个不存在的元素时:

> driver
    .elementByName('Google')
    .then(console.log.bind(console), console.log.bind(console))
  // Error: [elementByName("Google")] Error response status: 7, , NoSuchElement - An element could not be located on the page using the given search parameters.

进阶

1、BDD(Behavior-driven development)

       为什么我会介绍BDD? 因为Mocha就基于BDD思想的测试框架,并且我估计会有人把 BDD 和 链式调用 的概念搞混。

       BDD(Behavior Driven Development:行为驱动开发),是基于TDD发展的一种解决问题的思想,通过用类似自然语言方式描述软件行为,以达到可读性更高(让非技术人员也可以看懂)。

       以上测试代码中,属于BDD部分由Mocha提供的,如:describe, it, before, after…这些均为BDD风格的接口。如果是TDD风格(如:XUnit)的接口则是:suite, test, setup, teardown…

2、链式调用

       (1)什么是链式调用

driver
  .native()
  .elementByName('PERSONAL')
  .click()
  .sleep(1000)
  .takeScreenshot()
  .elementByName('Logout')
  .click()
  .sleep(1000)
  .takeScreenshot();

       以上代码组织方式为:链式调用

       如果你之前把BDD和链式调用搞混,估计看过以下代码:

When(...).Then(...).And(...).Should(...)

       这段代码就是BDD接口以链式方式调用,可读性非常高!但关于BDD的部分其实还是:When、Then、And、Should...

       (2)为何Macaca测试案例使用链式调用风格?

       某些情况下,使用链式调用方式书写代码是很舒服的,如C#的linq:

var rs = user.Where(x => x.Length == 3).Select(x => x).ToList();

       但如果把所有测试操作(无论操作间有无关联)都用链式调用方式组合,就比较奇怪了。如:

    return driver
       .webview()
       .elementById('pushView')
       .tap()
       .sleep(5000)
       .webview()
       .elementById('popView')
       .tap()
       .sleep(5000)
       .takeScreenshot();

       上面2个webview element的操作是没有任何关系的。而使用链式调用的场景一般是前后依赖、连续操作、层级递进,如上面的linq例子:where的结果集,接着要进行数据提取,然后是再把集合封装为list结构。

       所以,基于链式调用的原意,上面的案例的写法就有些奇怪了,并且Node.js的新手也不习惯。然而,为啥作者会写出这种的测试代码?原因在于:Node.js这个语言!

       Node.js是异步编程语言,例子如下:

  var el = driver.webview().elementById('hyddd')
  el.tap()

       上面2句,同步编程语言是怎么理解呢?

  1. 获取hyddd的element;
  2. 对element进行tap()操作;

       但换作异步编程语言呢?

  1. 获取hyddd的element;
  2. el.tap()同时于1执行,也就说,el还没赋值,2就已经开始执行了,完全没等1返回2就执行了;

       没法好好玩耍了,如果原生Node.js程序时要处理同步场景,就会出现所谓的callback hell,为了避免callback hell,就出现了Promise模式。嗯,在上面的测试代码中是不是看到这个单词?它作用就是把异步模式变为同步模式,同时避免callback hell。而它的表现就是现在这种链式调用!!!所以测试案例长得比较奇怪是开发语言导致的。

       参考文章:
       编写移动端 Macaca 测试用例 [单步调试]

       Macaca 异步测试用例编写进阶

       那些年我们玩过的手势

       如何从头编写你的 Macaca 用例

       Macaca-iOS入门那些事2

你可能感兴趣的:(#,自动化测试工具之macaca,iOS自动化测试)