编写 Instrumentation 测试

旧的 Glean 格言

如果某个内容重要到需要进行 Instrumentation,那么它也重要到需要进行测试。

Glean SDK 中的所有指标和 Ping 都有 完善的测试 API 文档。您需要熟悉 TestGetValue()(这是 一些指标的 JS (xpcshell) 测试示例)用于指标,以及 TestBeforeNextSubmit()(这是 自定义 Ping 的 C++ (gtest) 测试示例)用于 Ping。

所有测试 API 都可在 FOG 支持的三种语言中使用:Rust、C++ 和 JavaScript。

但是,如何才能进入可以调用这些测试 API 的状态?它们如何与 Firefox 桌面版测试框架相适应?

手动测试和调试

Glean SDK 具有 调试功能,用于手动验证 Instrumentation 是否已发送到 Mozilla 的数据管道。Firefox 桌面版通过环境变量和 about:glean 上的接口支持这些功能。

这对获得对当前情况良好运行的良好直观了解非常有用,但为了检查将来一切是否保持良好,您需要编写一些自动化测试。

需要牢记的一般事项

  • 您可能会看到来自先前测试的值在测试之间持续存在,因为配置文件目录在测试用例之间共享。

    • 您可以通过调用 Services.fog.testResetFOG()(在 JS 中)在测试前重置 Glean。

      • 如果您的 Instrumentation 不在父进程中,则应在 testResetFOG 之前调用 await Services.fog.testFlushAllChildren()。这将确保所有挂起的 data 都发送到父进程以进行清除。

    • 您无需在 C++ 或 Rust 中执行此操作,因为在这些语言中,您应该使用 FOGFixture 测试夹具。

  • 如果您的指标基于计时 (timespantiming_distribution),则不要期望能够断言正确的计时值。Glean 在 SDK 内部为您执行了很多计时操作,因此除非您模拟系统的单调时钟,否则不要期望值是可预测的。

    • 相反,请检查值是否 > 0 或样本数量是否符合预期。

    • 您可能能够断言该值至少与已知计时值一样多,但请注意舍入误差。

    • 如果您的指标是 timing_distribution,通过 GIFFT 镜像到遥测探测器,则系统之间可能会存在 细微的观察差异,这会导致相等性断言失败。

  • Instrumentation API 中的错误不会 panic、抛出异常或崩溃。但 Glean 会记住这些错误已发生。

    • 另一方面,测试 API 允许(有些人可能会说“鼓励”)在出现不良行为时 panic、抛出异常或崩溃。

    • 如果调用测试 API 并导致 panic、抛出异常或崩溃,则表示您的 Instrumentation 执行了错误的操作。检查您的测试日志以获取有关发生错误的详细信息。

测试和 Artifact 构建

Artifact 构建支持由 JOG 子系统 提供。它能够在运行时注册所有指标和 Ping 的最新版本。但是,编译后的代码仍在针对 Artifact 编译时当前版本的这些指标和 Ping 运行。

除非以下情况,否则这不是问题

  • 您正在更改编译代码中 Instrumentation 使用的指标或 Ping,或者

  • 您正在为编译代码中提交的 Ping 在 JavaScript 中使用 testBeforeNextSubmit

如有疑问,只需在 Artifact 模式下测试您的新测试(例如,将 --enable-artifact-builds 传递给 mach try),然后再提交。如果由于这两种情况之一,它在 Artifact 模式下未通过,则可能需要在启用 FOG 的 Artifact 构建支持时跳过测试

  • xpcshell

add_task(
  { skip_if: () => Services.prefs.getBoolPref("telemetry.fog.artifact_build", false) },
  function () {
    // ... your test ...
  }
);
  • mochitest

add_task(function () {
  if (Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)) {
    Assert.ok(true, "Test skipped in artifact mode.");
    return;
  }
  // ... your test ...
});

常用的测试格式

Instrumentation 测试倾向于遵循相同的三个部分格式

  1. 断言指标中没有值

  2. 表达行为

  3. 断言指标中的正确值

您选择的测试套件将取决于如何表达 Instrumentation 行为。

xpcshell 测试

如果 Instrumentation 行为位于主进程或内容进程上,并且可以从特权 JS 调用,则 xpcshell 是一个极好的选择。

但是,xpcshell 是一个非常小的环境,因此(待 bug 1756055 修复)您需要手动告诉它您需要两件事

  1. 配置文件目录

  2. 已初始化的 FOG

/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

add_setup(function test_setup() {
  // FOG needs a profile directory to put its data in.
  do_get_profile();

  // FOG needs to be initialized in order for data to flow.
  Services.fog.initializeFOG();
});

从那里开始,只需遵循常用的测试格式即可

add_task(function test_instrumentation() {
  // 1) Assert no value
  Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue());

  // 2) Express behaviour
  // ...<left as an exercise to the reader>...

  // 3) Assert correct value
  Assert.equal(kValue, Glean.myMetricCategory.myMetricName.testGetValue());
});

如果您的新 Instrumentation 包含新的自定义 Ping,则常用的测试格式有两个小的补充

  • 1.1) 在提交 Ping 之前 调用 testBeforeNextSubmit。您在 testBeforeNextSubmit 中注册的回调将与对 Ping 的 submit() 的调用同步调用。

  • 3.1) 检查 Ping 是否确实已提交。如果所有断言都在 testBeforeNextSubmit 的闭包内,则此测试通过的另一种方式是不运行任何断言。

add_task(function test_custom_ping() {
  // 1) Assert no value
  Assert.equal(undefined, Glean.myMetricCategory.myMetricName.testGetValue());

  // 1.1) Set up Step 3.
  let submitted = false;
  GleanPings.myPing.testBeforeNextSubmit(reason => {
    submitted = true;
    // 3) Assert correct value
    Assert.equal(kExpectedReason, reason, "Reason of submitted ping must match.");
    Assert.equal(kExpectedMetricValue, Glean.myMetricCategory.myMetricName.testGetValue());
  });

  // 2) Express behaviour that sends a ping with expected reason and contents
  // ...<left as an exercise to the reader>...

  // 3.1) Check that the ping actually was submitted.
  Assert.ok(submitted, "Ping was submitted, callback was called.");
});

(( 我们承认这不是最符合人体工程学的设计。请关注 bug 1756637 以获取有关 Ping 测试的更好设计和实现的更新。 ))

mochitest

browser-chrome 风格的 mochitests 可以像 xpcshell 一样进行测试,尽管您不需要请求配置文件或初始化 FOG。plain 风格的 mochitests 尚未得到支持(请关注 bug 1799977 以获取更新和解决方法)。

如果您在 mochitest 中进行测试,则您的 Instrumentation(或您的测试)可能未在父进程中运行。这意味着您将学习 IPC 测试 API。

IPC

所有测试 API 必须在主进程上调用(否则会断言)。但是您的 Instrumentation 可能会在任何进程上,那么如何测试它呢?

在这种情况下,常用的测试格式略有补充

  1. 断言指标中没有值

  2. 表达行为

  3. 使用 await Services.fog.testFlushAllChildren() 刷新所有挂起的 FOG IPC 操作

  4. 断言指标中的正确值。

**注意:**我们在 bug 1843178 中了解到,Services.fog.testFlushAllChildren() 使用的所有内容进程的列表在调用 BrowserUtils.withNewTab(...) 结束后的更新速度非常快。如果您使用 withNewTab,则应考虑在回调内部调用 testFlushAllChildren()

GTests/Google 测试

编写测试时,请使用 FOGFixture 测试夹具,例如

TEST_F(FOGFixture, MyTestCase) {
  // 1) Assert no value
  ASSERT_EQ(mozilla::Nothing(),
            my_metric_category::my_metric_name.TestGetValue());

  // 2) Express behaviour
  // ...<left as an exercise to the reader>...

  // 3) Assert correct value
  ASSERT_EQ(kValue,
            my_metric_category::my_metric_name.TestGetValue().unwrap().ref());
}

该测试夹具将负责确保在测试之间重置存储。

Rust rusttests

首先复习一下通用的 在 Firefox 中测试和调试 Rust 代码 非常有帮助。

不幸的是,FOG 需要 gecko(以告知它配置文件目录在哪里以及其他内容),这意味着我们需要使用 GTest + FFI 方法,其中 GTest 是运行器,而 Rust 只是编写测试的语言。

这意味着您的测试将类似于以下 GTest

extern "C" void Rust_MyRustTest();
TEST_F(FOGFixture, MyRustTest) { Rust_MyRustTest(); }

以及类似以下的 Rust 测试

#[no_mangle]
pub extern "C" fn Rust_MyRustTest() {
    // 1) Assert no value
    assert_eq!(None,
               fog::metrics::my_metric_category::my_metric_name.test_get_value(None));

    // 2) Express behaviour
    // ...<left as an exercise to the reader>...

    // 3) Assert correct value
    assert_eq!(Some(value),
               fog::metrics::my_metric_category::my_metric_name.test_get_value(None));
}