第07课:端到端测试和发布应用

第07课:端到端测试和发布应用

在前面的文章中,我们已经为组件添加了单元测试。单元测试针对的是单一组件。在测试时一般使用的是服务的 mock 对象,并不是真正的服务本身。单元测试有利于隔离不同的组件,确保每个组件自身的质量。不过,单元测试并不能测试出多个组件协同工作的情况,或是测试应用在使用真实数据时的行为,或是测试一个完整的用户使用流程。这些测试都需要端到端(End-to-End)测试来完成。

端到端测试

在浏览器中运行

Angular 应用使用 Protractor 进行端到端测试。Protractor 测试时会启动浏览器来访问待测试的应用。这就要求应用可以直接在浏览器中运行。而实例应用使用了 Cordova HTTP 插件来获取服务器端数据。在缺乏 Cordova 插件支持的浏览器中是无法直接运行的。为了能够进行测试,需要首先解决应用在浏览器中运行的问题。

Angular 应用可以使用 HttpClient 来访问服务器端。不过 HttpClient 会受到浏览器的同源访问限制,不能访问跨域资源。而目前数据存放的服务器和测试环境不在同一个域下。为了解决这个问题,可以在服务器端允许跨域访问请求,即启用 CORS。不过这种方式要求对服务器进行额外的配置。

另一种方式是使用代理服务器来进行访问。对于 Ionic 应用来说,使用代理服务器是最简单的做法,因为 Ionic 的开发服务器就内置了对代理访问的支持。只需要在 Ionic 应用的配置文件 ionic.config.json 中进行配置即可。新增加的配置项 proxies 中定义了从 /api 到 https://vividcode.io/quwen 的代理,也就是说,访问 /api 的请求会被代理给实际的 URL:https://vividcode.io/quwen。比如,访问 /api/news.json 的响应来自 https://vividcode.io/quwen/news.json。

{
  "name": "quwen",
  "app_id": "",
  "type": "ionic-angular",
  "integrations": {
    "cordova": {}
  },
  "proxies": [
    {
      "path": "/api",
      "proxyUrl": "https://vividcode.io/quwen"
    }
  ]
}

接着就需要修改 ItemsListService 来根据不同的平台使用不同的数据获取方式。HttpClient 所在的模块 HttpClientModule 需要在 AppModule 中导入。

下面是修改后的 ItemsListService。首先是增加了注入的服务 HttpClient。在 platform.ready 方法返回的 Promise 完成时,其完成值 readySource 就表明了底层的平台。cordova 表示是 Cordova 平台。在 Cordova 上,仍然使用之前的 HTTP 插件来获取数据。如果不是 Cordova 平台,则使用 HttpClient 来获取数据。数据访问的 URL 需要首先转换成访问代理的 URL。

import { Item, ItemsList } from "../models/item";
import { Injectable } from "@angular/core";
import { HTTP } from '@ionic-native/http';
import { Platform } from "ionic-angular";
import { HttpClient } from "@angular/common/http";

@Injectable()
export class ItemsListService {

  constructor(private platform: Platform, private http: HTTP, private httpClient: HttpClient) {}

  load(url: string, converter: (data: any) => Item[]): Promise<ItemsList> {
    return this.platform.ready().then((readySource) => {
      if (readySource == 'cordova') {
        return this.http.get(url, {}, {}).then(res => {
          let data = JSON.parse(res.data);
          return {
            items: converter(data),
          };
        });
      } else {
        return this.httpClient.get(url.replace('https://vividcode.io/quwen', '/api')).toPromise().then(data => {
          return {
            items: converter(data),
          };
        });
      }
    });
  }
}

经过修改之后,可以在浏览器中看到显示的新闻列表。

在浏览器中运行

Protractor

现在就可以使用 Protractor 进行端到端的测试了。在根目录下添加 Protractor 的配置文件 protractor.conf.js。在这个配置文件中,specs 配置了要查找的测试套件文件,即 e2e 目录下的所有以 .e2e-spec.ts 结尾的文件。Protractor 的测试也是使用 Jasmine 编写的。capabilities 中指定了要使用的浏览器为Chrome。SELENIUMPROMISEMANAGER 的值为 false 表示不使用 Selenium WebDriver 自己的控制流程,还是通过 Promise 来进行管理。这样是为了在测试套件代码中使用 async/await。

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  SELENIUM_PROMISE_MANAGER: false,
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

在 Angular CLI 中已经对 Protractor 进行了配置,只需要运行 ng e2e 就可以启动 Protractor。同样需要在 package.json 中添加相应的脚本 e2e 来运行 ng e2e。

在编写测试套件时,推荐使用 Selenium 中的页面对象模式(Page Objects),也就是把对一个页面的操作都封装在一个对象中。这样使用起来更加简单,也便于维护和复用。下面的代码是 HomePage 对应的页面对象。其中的 navigateTo 方法用来访问该页面,即访问地址:http://localhost:8100,也就是 Ionic 开发服务器。方法 getItemsCount 用来获取到页面上条目的数量。browser.wait(EC.visibilityOf($('quwen-items-list'))) 用来等待页面上出现 quwen-items-list 元素,也就是说条目的列表已经完成加载并显示。 $$('quwen-item').count() 返回的是所有 quwen-item 元素的数量,也就是条目的数量。

import { $, $$, browser, protractor } from "protractor";

const EC = protractor.ExpectedConditions;

export class HomePage {
  navigateTo() {
    return browser.get('http://localhost:8100');
  }

  async getItemsCount() {
    await browser.wait(EC.visibilityOf($('quwen-items-list')));
    return await $$('quwen-item').count();
  }
}

HomePage 的端到端测试套件如下所示。在 beforeEach 方法中创建一个新的页面对象的实例。在测试规格中,首先等待页面加载完成,然后调用 getItemsCount 来获取到条目数量,最后验证条目数量是 20。这里我们通过 async/await 来简化了对 Promise 的使用。

import { HomePage } from "./home.po";

describe('Home page', () => {
  let page: HomePage;

  beforeEach(() => {
    page = new HomePage();
  });

  it('should show 20 items', async () => {
    await page.navigateTo();
    let items = await page.getItemsCount();
    expect(items).toEqual(20);
  });

});

当运行 ng e2e 时,Protractor 会自动启动 Chrome 并执行测试套件。测试的结果会输出在控制台。

[22:58:48] I/update - chromedriver: file exists /Users/fucheng/git/quwen/node_modules/webdriver-manager/selenium/chromedriver_2.34.zip
[22:58:48] I/update - chromedriver: unzipping chromedriver_2.34.zip
[22:58:48] I/update - chromedriver: setting permissions to 0755 for /Users/fucheng/git/quwen/node_modules/webdriver-manager/selenium/chromedriver_2.34
[22:58:48] I/update - chromedriver: chromedriver_2.34 up to date
[22:58:48] I/launcher - Running 1 instances of WebDriver
[22:58:48] I/direct - Using ChromeDriver directly...
Jasmine started

  Home page
    ✓ should show 20 items

Executed 1 of 1 spec SUCCESS in 2 secs.
[22:58:52] I/launcher - 0 instance(s) of WebDriver still running
[22:58:52] I/launcher - chrome #01 passed
✨  Done in 21.12s.

发布应用

在完成基本的开发和测试之后,需要在模拟器和实际设备上进行更多的测试。测试完成之后,就可以准备应用的发布。

图标和启动屏幕图片

应用发布之前的一个重要的工作是更新应用的图标和启动屏幕图片。为了适应不同平台和屏幕尺寸,需要生成大小各不相同的图标和启动屏幕图片。Ionic 命令行工具提供了 ionic cordova resources 命令让这个过程变得非常简单。只需要提供一个原始的图标和启动屏幕图片,该工具会自动生成不同平台所需要的大小各异的文件。

原始图标要求大小至少是 1024x1024 的 PNG 文件,启动屏幕图片要求大小至少是 2732×2732 的 PNG 文件。把图标文件命名为 icon.png,把启动屏幕文件命名为 splash.png,并放到 resources 目录下,再运行 ionic cordova resources 命令即可。ionic cordova resources 命令会生成 iOS 和 Android 平台上的图标和启动屏幕图片。可以使用 ionic cordova resources ios 或 ionic cordova resources android 来针对特定平台生成。也可以使用参数 --icon 或 --splash 来只生成图标或启动屏幕图片。

$ ionic cordova resources
$ ionic cordova resources ios
$ ionic cordova resources android --icon
$ ionic cordova resources ios --splash

需要注意的是,ionic cordova resources 命令需要使用 Ionic 提供的服务,因此需要有一个 Ionic 的账号并登录之后,才能使用 ionic cordova resources。

下图给出了实例应用添加图标后的效果。

应用图标

构建发布版本

在构建发布版本的应用之前,需要对 Ionic 应用的一些配置进行更新。首先是修改根目录下 config.xml 中的应用基本信息,包括 name、description 和 author 等。其中最重要的是<widget>元素的属性 id。属性 id 的值会被作为 iOS 和 Android 平台上应用的标识符。该属性的默认值是 io.ionic.starter。必须要修改为应用特有的唯一值。当修改了属性 id 的值之后,需要使用 ionic cordova platform update 命令来更新平台。不过 ionic cordova platform update 命令不支持 iOS 平台的更新。为了稳妥起见,最好的做法是使用 ionic cordova platform rm 来首先删除平台,再使用 ionic cordova platform add 来重新添加。由于平台相关的文件完全是通过构建生成的,重新删除并添加不会有什么影响。一般是不建议手动更新 platforms 目录下的平台相关文件。如果做了修改,删除并重新添加平台会导致这些修改丢失,需要在删除之前先把改动的文件复制出来。重新添加平台之后再恢复回去。

构建 Ionic 应用使用的是ionic cordova build <platform>命令,如 ionic cordova build ios 和 ionic cordova build android。该命令有如下参数:

  • --prod:针对生产环境构建应用;
  • --aot:启动 Angular 的预先编译(Ahead-of-Time Compilation)功能;
  • --minifyjs:精简 JavaScript 代码;
  • --minifycss:精简 CSS 代码;
  • --debug:创建 Cordova 的调试 build;
  • --release:创建 Cordova 的发布 build;
  • --device:创建针对设备的 Cordova build;
  • --emulator:创建针对模拟器的 Cordova build。

以 Android 平台为例来说明。在运行 ionic cordova build android 时,需要传入 --prod 和 --release。还需要传入与发布版本签名相关的信息。这些参数放在 -- -- 之后,包括签名的密钥库文件的路径和密码,以及密钥的别名和密码。

$ ionic cordova build android --prod --release -- -- --keystore="<keystore file>" --alias=<key alias> --storePassword=<store password> --password=<key password>

在完成构建之后,可以找到构建之后的 apk 文件(platforms/android/build/outputs/apk/android-release.apk)。可以使用 adb install 命令把该 apk 文件安装到设备上进行测试。测试完成之后可以发布到应用商店。

下图是实例应用在 Android 设备上的最终运行效果。

最终应用的运行效果

至此,本课程的内容全部结束。希望通过本课程的学习,可以让你学会如何使用 Ionic 3 进行移动应用的开发。

本课程的示例应用的完整源代码见 GitHub

上一篇
下一篇
目录