功能测试编辑

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

运行功能测试编辑

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

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

node scripts/functional_test_runner

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

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

编写功能测试编辑

环境编辑

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

我们使用 chromedriverleadfootdigdug 在 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

术语编辑

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。每个测试文件都必须定义唯一一个顶级测试套件,但测试套件可以拥有任意多个嵌套测试套件。
测试文件剖析编辑

下列带注释的示例文件展示了每个测试套件所使用的基本结构。它首先导入 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编辑

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

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

log

ToolingLog 的一个准备使用的实例

readConfigFile(path)

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

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

getService(name)

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

getPageObjects(names)

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

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

loadTestFile(path)

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

服务指标编辑

内置服务编辑

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 中
  • 公开生命周期事件以进行基本协调。处理程序可以返回承诺并异步地解析、失败
  • 包括 beforeLoadTestsbeforeTestsbeforeEachTestcleanupphaseStartphaseEnd 阶段
Kibana 服务编辑

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:
自定义服务编辑

服务是有意通用的。它们可以是任何东西(甚至什么都不是)。有些服务有助于与特定类型的 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编辑

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

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

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

Gotchas编辑

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

功能测试计时编辑

另一个重要的 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更新的假设。

调试编辑

在命令行运行:

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’);