IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

功能测试

edit

我们通过功能测试确保 Kibana UI 按照预期方式运行。该功能测试通过自动化用户交互,取代了长达数小时的人工测试。Kibana 使用的工具叫做 FunctionalTestRunner ,能够更好的控制功能测试环境,更便于插件作者使用。

运行功能测试

edit

FunctionalTestRunner 结构非常简单,大部分功能主要源自位于 test/functional/config.js 的配置文件。如果您正在编写一个插件,就会拥有自己的配置文件。有关更多信息,请参见 插件功能测试 。

使用 node.js 执行 FunctionalTestRunner 脚本运行测试,使用 Kibana 的默认配置:

node scripts/functional_test_runner

在没有任何参数的情况下运行时, FunctionalTestRunner 会自动加载位于标准位置的配置,但是您可以用 --config 标记来覆盖这个行为。 --bail 和 --grep 也有命令行标记,功能与 mocha 类似。 日志也可以通过使用 --quiet 、 --debug 或 --verbose 标记进行自定义设置。

使用 --help 标记获得更多选项。

编写功能测试

edit
环境
edit

测试位于 mocha ,使用 expect 作断言。

我们使用 chromedriver、 leadfoot 和 digdug 在 Chrome 上做自动化测试。 FunctionalTestRunner 启动后,digdug 会打开一个启动 chromedriver 的 Tunnel 和一个 Chrome 的精简用例。digdug 还会创建一个 Leadfoot’s Command 类的用例,该用例可以通过 remote 服务器获取。 remote 通过 digdug Tunnel 与 Chrome 进行通讯。 remote 涉及的所有命令详见 leadfoot/Command API。

FunctionalTestRunner 使用 babel 语言自动编译功能测试,因此测试可以使用 Kibana 源代码使用的 ECMAScript 特性。详见 style_guides/js_style_guide.md。

术语
edit

Provider(提供者):

FunctionalTestRunner 运行的代码会被打包进一个函数中,这样它就可以通过配置文件传递,并且能被参数化。这些提供者函数中的任何一个都有可能是异步的,并需要返回或重新获取他们想要的 值 。提供者函数总通过单一参数:API Provider(参见 Provider API 章节)来调用。

提供者配置示例:

// config and test files use `export default`
export default function (/* { providerAPI } */) {
  return {
    // ...
  }
}
Services(服务)
服务根据服务提供者产生的单一值命名。测试和其他服务能够通过请求服务的名称来检索服务实例。除 mocha API 以外的所有功能都是通过服务公开的。
Page objects(页对象)
页对象是一种将通常行为封装进特定页面或插件的特殊服务。当您编写自己的插件时,您可能想要添加一个(或多个)用于描述测试所需执行的常见交互的页面对象。
Test Files(测试文件)
FunctionalTestRunner 的主要目的是执行测试文件。这些文件导出一个提供者 API 调用的测试提供者(Test Provider),但并不会返回数值。相反,测试提供者用 mocha’s BDD interface 定义一个程序组。
Test Suite(测试套件)
测试套件是调用 describe() 的测试集,然后通过调用 it() 、 before() 、 beforeEach() 等填充测试和 setup/teardown hooks。每个测试文件都必须定义唯一一个顶级测试套件,但测试套件可以拥有任意多个嵌套测试套件。
测试文件剖析
edit

下列带注释的示例文件展示了每个测试套件所使用的基本结构。它首先导入 expect.js,然后定义其默认输出:一个匿名的测试提供者。该测试提供者为 getService() 和 getPageObjects() 函数拆解提供者 API。它使用这些函数来收集本套件的依赖。对于 mocha.js 用户,其他测试文件看起来就很普通了。

import expect from 'expect.js';
// test files must `export default` a function that defines a test suite
export default function ({ getService, getPageObject }) {

  // most test files will start off by loading some services
  const retry = getService('retry');
  const testSubjects = getService('testSubjects');
  const esArchiver = getService('esArchiver');

  // for historical reasons, PageObjects are loaded in a single API call
  // and returned on an object with a key/value for each requested PageObject
  const PageObjects = getPageObjects(['common', 'visualize']);

  // every file must define a top-level suite before defining hooks/tests
  describe('My Test Suite', () => {

    // most suites start with a before hook that navigates to a specific
    // app/page and restores some archives into elasticsearch with esArchiver
    before(async () => {
  await Promise.all([
        // start with an empty .kibana index
        esArchiver.load('empty_kibana'),
        // load some basic log data only if the index doesn't exist
        esArchiver.loadIfNeeded('makelogs')
      ]);
      // go to the page described by `apps.visualize` in the config
      await PageObjects.common.navigateTo('visualize');
    });

    // right after the before() hook definition, add the teardown steps
    // that will tidy up elasticsearch for other test suites
    after(async () => {
      // we unload the empty_kibana archive but not the makelogs
      // archive because we don't make any changes to it, and subsequent
      // suites could use it if they call `.loadIfNeeded()`.
      await esArchiver.unload('empty_kibana');
    });

    // This series of tests illustrate how tests generally verify
    // one step of a larger process and then move on to the next in
    // a new test, each step building on top of the previous
    it('Vis Listing Page is empty');
    it('Create a new vis');
    it('Shows new vis in listing page');
    it('Opens the saved vis');
    it('Respects time filter changes');
    it(...
  });

}

提供者 API

edit

提供者 API 对象(Provider API Object)是所有提供者的第一个也是唯一一个参数。这个对象可以用于加载服务、页面对象和配置、测试文件。

在配置文件中,API具有以下属性

log

ToolingLog 的一个准备使用的实例

readConfigFile(path)

返回一个解析为配置实例的承诺,提供 path 路径下的配置文件值

在服务和 PageObject 提供者中,API 是:

getService(name)

根据名称,加载并返回服务的一个单例实例

getPageObjects(names)

加载 PageObject 的单例实例,收集它们到一个对象,名字是 PageObject 中每个对象的 key

测试提供者中的 API 与服务提供者 API 相同,但是具有附加方法:

loadTestFile(path)

加载路径上的测试文件。使用此方法将其他文件中的套件嵌套到更高级的套件中。

服务指标

edit
内置服务
edit

FunctionalTestRunner 自带三种内置 service:

config:
log:
  • 源码: src/utils/tooling_log/tooling_log.js
  • ToolingLog 实例是可读流。此服务提供的实例由 FunctionalTestRunner CLI 自动传输到 stdout
  • log.verbose() 、 log.debug() 、 log.info() 、 log.warning() 像 console.log 那样工作,只不过产生结构化更好的输出
lifecycle:
  • 源码: src/functional_test_runner/lib/lifecycle.js
  • 设计主要用于 service 中
  • 公开生命周期事件以进行基本协调。处理程序可以返回承诺并异步地解析、失败
  • 包括 beforeLoadTests 、 beforeTests 、 beforeEachTest 、 cleanup 、 phaseStart 、 phaseEnd 阶段
Kibana 服务
edit

Kibana 功能测试定义了绝大部分测试会使用的实际功能。

retry:
  • 源码: test/functional/services/retry.js
  • 重试操作辅助器
  • 常用方法:

    • retry.try(fn) - 在 loop 中执行 fn 直至成功或超过默认重试时间
    • retry.tryForTime(ms, fn) 在 loop 中执行,直至成功或超过 ms 毫秒
testSubjects:
  • 源码: test/functional/services/test_subjects.js
  • 测试主题是从测试中选出的被专门标记过的要素
  • 可能的情况下,在 CSS 选择器中使用 testSubjects
  • 使用:

    • 用 data-test-subj 属性标记您的测试对象:

      <div id="container”>
        <button id="clickMe” data-test-subj=”containerButton” />
      </div>
    • 使用 testSubjects 帮助器点击这个按钮

      await testSubjects.click(‘containerButton’);
  • 常用方法:

    • testSubjects.find(testSubjectSelector) - 在页面中寻找一个测试对象;如果过一段时间没有找到,抛出异常
    • testSubjects.click(testSubjectSelector) - 在页面中点击一个测试主题;如果过一段时间没有找到,抛出异常
find:
  • 源码: test/functional/services/find.js
  • remote.findBy 方法帮助器,用于记录日志和管理超时
  • 常用方法:

    • find.byCssSelector()
    • find.allByCssSelector()
kibanaServer:
esArchiver:
docTable:
pointSeriesVis:
Low-level utilities:
自定义服务
edit

服务是有意通用的。它们可以是任何东西(甚至什么都不是)。有些服务有助于与特定类型的 UI 元素(如 PooSosieServices )交互,而其他服务则更为基础,如日志或配置。每当您想在可重用包中提供一些功能时,请考虑制作自定义服务。

为了创建一个自定义的 somethingUseful service:

  • 创建一个如下的 test/functional/services/something_useful.js 文件:

    // Services are defined by Provider functions that receive the ServiceProviderAPI
    export function SomethingUsefulProvider({ getService }) {
      const log = getService('log');
    
      class SomethingUseful {
        doSomething() {
        }
      }
      return new SomethingUseful();
    }
  • 从 services/index.js 重新导出您的 provider
  • 将它导入到 src/functional/config.js 并添加到服务配置中:

    import { SomethingUsefulProvider } from './services';
    
    export default function () {
      return {
        // … truncated ...
        services: {
          somethingUseful: SomethingUsefulProvider
        }
      }
    }

PageObjects

edit

PageObject 的目的只是自我解释。可视化的 PageObject 提供与可视化 app 交互的助手,相当于仪表板对于仪表板 app。

"common" PageObject 是一个例外。作为一个延缓的实验性的实现,common PageObject 是有用的跨页面的帮助器集合。现在我们有了共享服务,并且这些服务可以与其他的 FunctionalTestRunner 共享,我们会继续将功能从 common PageObject 转移到服务中。

请在已有或新服务中添加新的方法,而不是进一步扩展 CommonPage 类。

Gotchas

edit

记住您不能运行文件( it 块)中一个单独的测试,因为整个 describe 需要按顺序执行。在一个文件中应该只有一个顶级的 describe 。

功能测试计时
edit

另一个重要的 gotcha 是通过注意时间来编写稳定的测试。所有 remote 方法异步运行。最好在进入下一步之前,在 UI 上添加等待变化的交互。

例如,与其简单的编写点击按钮的交互,不如在头脑中编写更高级目的的交互:

不好的例子: PageObjects.app.clickButton()

class AppPage {
  // what can people who call this method expect from the
  // UI after the promise resolves? Since the reaction to most
  // clicks is asynchronous the behavior is dependant on timing
  // and likely to cause test that fail unexpectedly
  async clickButton () {
    await testSubjects.click(‘menuButton’);
  }
}

好的例子: PageObjects.app.openMenu()

class AppPage {
  // unlike `clickButton()`, callers of `openMenu()` know
  // the state that the UI will be in before they move on to
  // the next step
  async openMenu () {
    await testSubjects.click(‘menuButton’);
    await testSubjects.exists(‘menu’);
  }
}

这样写将确保您的测试时间不是片状的,或者基于交互后UI更新的假设。

调试

edit

在命令行运行:

node --debug-brk --inspect scripts/functional_test_runner

该命令会输出一个URL,通过在 Chrome 浏览器中访问该URL,您可以调试您的功能测试用例。

您也可以在运行 FunctionalTestRunner 时增加 --debug 或 --verbose 参数,从而在命令行看额外的日志信息。您可以像下面这样,在您的测试用例中增加日志:

// load the log service
const log = getService(‘log’);

// log.debug only writes when using the `--debug` or `--verbose` flag.
log.debug(‘done clicking menu’);