在快应用中使用自动化测试

快应用教程 Dec 09, 2020

测试是软件开发中必不可少的一个环节,程序化测试能够加快研发速度,提高协作效率,减少产品故障。

传统测试的分类

传统前端项目的测试,可以从两个维度去做分类:

粗细粒度的角度

从粗细粒度的角度看,测试分为以下几类:

  • 单元测试

主要针对 JS 中的某些方法(不包括 ux 中的定义),这些方法独立性强,不强依赖于外部环境;

这类需求有明确的输入输出要求,通过常规的测试框架即可完成,如:mochaJest

  • 集成测试

主要针对功能比较完整的模块或者组件,它们是多个单元/模块的组装与业务结合;

这类需求希望面对常见的业务应用场景,能够得到正确的界面渲染与数据结构;

在当前的快应用平台中,如果用到自定义组件或者底层接口(如:@system.fetch),需要依赖于真机环境进行测试确认结果;

当然,部分开发者希望能够提供一个模拟环境(如:NodeJS)的快应用平台,方便在 PC 上完成测试,目前这个能力仅团队内部使用,考虑到更新频次较高,暂没有对外开放;

实际上,优先推荐开发者使用真机环境通过自动化的方式完成确认,这样可以确保多手机厂商设备下对功能的统一能力确认;

针对这类测试的实现,开发者可以考虑在快应用项目中建立新的能力测试页面,引入待测试的自定义组件与模块,通过mochaJest等工具完成断言;

  • e2e 测试

主要针对项目与页面级别的测试,确保项目的基本功能畅通,是对项目上线的一个主要保障;

这类需求的原理就是通过真实的浏览器去运行每个页面,模拟用户行为操作,确保界面的一致性,功能正确;

对于 WEB 的前端开发者通过KarmaSelenium等工具,完成对浏览器操作的自动化封装,最终测试页面与后端服务器的正确配合;

对于快应用的前端开发者,需要借助于一些接口与简单类库封装,来承载页面的加载、切换等测试任务;

功能覆盖的角度

从功能覆盖的角度,测试可以分为几类:

  • 接口测试

主要针对底层为前端开发者提供的接口,确保这些接口在跨设备上行为和输出正确;如:@system.storage

  • 界面测试

主要针对页面局部的 UI 布局渲染正确,确定:文本、对话框、滑动等节点存在,位置正确;

  • 功能测试

主要针对某些行为操作下的功能表现正确,或者小到一个模块、一个方法的输出正确;

如何测试快应用

在快应用的项目中,如果开发者仅仅只是 JS 文件中方法的单元测试的话(不需要引入底层接口),完全可以通过自己引入mocha等工具,然后运行在 PC 的 NodeJS 环境来实现,这块实现简单,本文不赘述;

当前快应用的实现中,框架为开发者提供了一套 e2e 的测试框架,这类测试需要运行在真实的手机设备中,然后配合@system.router接口完成页面之间的切换与内容测试,后面介绍原理;

开发者可以通过以下步骤来为项目引入 e2e 的测试能力:

1. 新建示例项目

使用命令行新建一个自定义项目,名称为:quickapp-demo-quality

  npx hap init quickapp-demo-quality

或者也可以通过安装快应用开发工具来新建项目

当然,开发者也可以使用自己已有的项目,用于增加测试能力;

提示:为了方便开发者理解并使用,快应用官方的Github 站点提供了示例项目

2. 添加并编写测试用例

在项目中,创建test目录,与src目录同级,该文件夹用于存放所有的页面测试用例;

其中针对每个页面的测试用例的文件路径需要与src目录中对应页面的路径保持一致;

当前项目我们添加DemoDemoDetailAbout三个页面的测试用例。结构如下:

testing-1

其中针对Demo页面的测试用例,举例如下,其它测试用例的文件内容类似:

/**
 * @param vm 代表页面的ViewModel实例
 */
export default function (vm) {
  describe(`Demo`, function () {
    it(`测试Detail页面vm属性`, function (done) {
      //vm存在一个属性title,类型为string且值为'示例页面'
      expect(vm.title).to.be.a("string").to.be.equal("示例页面");
      done();
    });

    it(`测试Detail页面vm方法`, function (done) {
      //vm存在一个方法add,用于将两个参数相加
      expect(vm.add(1, 1)).to.be.equal(2);
      done();
    });
  });
}

3. 生成要测试的页面

test目录下创建一个 JS 文件autocase.js,表示所有要测试的页面文件列表,其内容如下:

其中,title 是测试名,name 描述了本次要测试的页面:DemoDemoDetailAbout,params 用于给 vm 中传入一些用于测试额外参数;

const autoCaseList = [
  {
    title: "case101",
    name: "Demo",
    params: {},
  },
  {
    title: "case201",
    name: "DemoDetail",
    params: {
      caseTitle: "case201",
      externalString: "foo",
    },
  },
  {
    title: "case202",
    name: "DemoDetail",
    params: {
      caseTitle: "case202",
      externalString: "bar",
    },
  },
  {
    title: "case301",
    name: "About",
    params: {},
  },
];

export { autoCaseList };

该文件供下面的测试汇总页面使用,声明具体需要执行的测试。

4. 增加测试汇总页面与自动化能力

上一步仅代表哪些页面需要进行测试,并测试页面中的哪些能力;

这一步主要完成两件事:

  1. 增加测试汇总页面,记录测试结果;

  2. 将各测试页面的结果与切换连接起来,形成自动化;

src目录下创建一个测试汇总的页面Summary并在manifest.json中声明路由;

页面内容中的 JS 代码部分举例如下:

<script>
  import router from "@system.router";
  import fetch from "@system.fetch";
  import prompt from "@system.prompt";

  import { autoCaseList } from "../../test/autocase";

  /**
   * 获取下一个自动测试的page
   */
  function findNextTestPage() {
    const list = global.loadData("pageNameList");
    const item = list.shift();
    global.saveData("pageNameList", list);
    return item;
  }

  function waitForOK(time = 100) {
    return new Promise((resolve) => {
      setTimeout(resolve, time);
    });
  }

  export default {
    private: {
      // 包含自动测试脚本的case列表
      pageNameList: [],
      pageTestList: [],
      shouldTestAll: false,
      showCompletedText: false,
      isRunningTest: false,
    },
    onInit() {
      this.pageNameList = autoCaseList;
      // 初始化自动化测试相关数据
      if (global.loadData) {
        global.saveData("pageNameList", this.pageNameList);
      }
    },
    onShow() {
      // 更新pageTestList
      this.pageTestList = (global.loadData("pageTestList") || []).map(
        (item) => {
          item.showPageTestDetail = false;
          item.tests.forEach((itemCase) => {
            itemCase.showPageTestErrDetail = false;
          });
          return item;
        }
      );
      this.shouldTestAll && this.startNextTestPage();
    },
    /**
     * 重启整个所有测试
     */
    restartTestProcess() {
      // 防止连续多次点击
      if (!this.isRunningTest) {
        this.isRunningTest = true;
        global.saveData("pageNameList", this.pageNameList);
        global.saveData("pageTestList", []);

        this.pageTestList = [];
        // 重置测试结束文本的显示状态
        this.showCompletedText = false;
        // 自动跑测试下一个测试用例
        this.shouldTestAll = true;
        // 启动下个测试用例
        this.startNextTestPage();
      }
    },
    /**
     * 启动下个测试用例
     */
    async startNextTestPage() {
      const pageItem = findNextTestPage();
      if (pageItem) {
        console.info(`下个测试用例:${pageItem.title}:${pageItem.name}`);

        await waitForOK(1000);
        console.info(`开始测试页面:${pageItem.title}:${pageItem.name}`);
        router.push({
          uri: pageItem.name,
          params: pageItem.params,
        });
      } else {
        console.info(`测试用例列表执行完毕`);

        this.isRunningTest = false;
        this.showCompletedText = true;
        this.shouldTestAll = false;
      }
    },
    gotoPage(path, params) {
      // 单个页面的点击跳转:不会在测试后,自动返回
      params = Object.assign(
        {
          back: "false",
        },
        params
      );

      router.push({
        uri: path,
        params,
      });
    },
    togglePageTestDetailStatus($item) {
      $item.showPageTestDetail = !$item.showPageTestDetail;
    },
    togglePageErrStackStatus($item) {
      $item.showPageTestErrDetail = !$item.showPageTestErrDetail;
    },
  };
</script>

提示:开发者可以在官方站点的示例项目中查看该页面全部内容,路径为:src/Summary/index.ux

5. 构建自动化测试的 RPK 文件

在项目目录下,执行构建命令 npm run build:test,并运行在快应用平台,即可完成自动化测试;(注意,如果运行命令后报错,可以 npm 安装@babel/helper-compilation-targets 解决)

打开页面并点击按钮点击重新测试,完成一整套自动化测试过程。

最终的示例效果如下:

testing-41

方案实现原理

上面这种 e2e 的测试方式,并不需要修改框架运行时,即:不需要前端框架配合修改某些代码;

相反,它的实现主要是通过:上面的开发者代码与hap-toolkit编译时工具在启用参数--enable-e2e构建后注入的代码配合完成的;

下面从编译时、运行时两个方面,介绍实现原理,方便开发者理解,并进行更深程度的定制与改造;

编译时

  1. 开发者执行构建命令npm run build:test,将会启用参数--enable-e2e来创建 RPK 文件;开发者可以通过package.json查看细节;
  2. 该参数启用后,hap-toolkit将会完成以下几件事,其中前两步可以通过build/app.js文件查看细节,后两部通过build目录下对应的页面 JS 查看细节;
  3. 向项目的app.ux中,注入测试相关类库:hybrid-mochahybrid-chai,他们分别是对类库mochajschaijs的简单适配的封装;
  4. 向项目的app.ux中,注入一些全局函数:loadData(key)saveData(key, value)提供给每个页面调用,这两个函数分别用于向 JS 内存中全局获取数据与保存数据;
  5. 向项目的页面级 ux 文件中,关联引入test目录中对应的测试用例文件(相对路径保持一致的 JS 文件);
  6. 向项目的页面级 ux 文件中,注入mocha实例化与运行的代码,伪代码如:const mocha = new Mocha(); mocha.run();
  7. hap-toolkit走正常流程,编译每个页面,如:汇总页面 Summary,并生成 RPK 文件;

运行时

  1. 快应用启动时,先加载 RPK 中的app.js,即:源码中的app.ux
  2. 上一步接着会向全局环境注入mochaassertexpectshould测试类库,与全局函数loadData(key)saveData(key, value)
  3. 接着根据manifest.json的定义,加载首页Summary,呈现汇总页面的初始状态,此时还没有执行任何的页面测试;
  4. 开发者点击页面中的按钮点击重新测试,就会执行对应的方法restartTestProcess(),该方法将会依次加载变量autoCaseList中每个页面,直到测试完成;
  5. 在拥有测试用例的每个页面中,会依次实例化mocha,并完成test目录下对应的测试 JS 文件的执行,并得到测试结果,最后返回到汇总页面Summary
  6. 所有页面测试完成之后,返回到汇总页面Summary,此时会展现每个页面的执行结果;开发者可以点击每条记录,查看正确与出错的测试详情;

其它参考

总结

当前快应用的测试方式与程序化能力,辅助开发者完成功能等上的保证,从而确定项目的稳定性,提升维护性。

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.