SuperAPI AI 工具集

meteor-testing

测试与质量

查看项目

更新于 10-15 15:35

技能介绍

用途

meteor-testing 用于在 Meteor 3 应用中搭建、设计、编写或修复测试与测试框架。当出现 meteortesting:mocha、--driver-package 等触发条件时使用。

meteortesting:mocha 是 Meteor 的标准测试驱动。它以测试模式启动应用(除测试文件外不运行应用代码),并在服务器控制台报告结果。

如果现有测试失败、挂起或出现不稳定,且不清楚失败层级,应先用 meteor-debugging 隔离原因;当证据指向测试搭建、设计、驱动行为、fixtures 或回归覆盖时,再回到本技能。

决策流程

  • 现有测试出现未知失败?使用 meteor-debugging 归类应用、数据、测试框架、浏览器、环境或共享状态等原因。
  • 纯逻辑、不涉及 Meteor API?使用普通 Mocha 或任意测试运行器,无需 Meteor 特定内容。
  • 方法或发布?用 meteortesting:mocha 做集成测试,使用 Meteor.server.method_handlers[name].apply(ctx, args) 或 publish_handlers。
  • 需要驱动真实 DDP 客户端(跨进程或端到端 DDP 语义)?使用 --full-app 模式和 DDP.connect。
  • UI 流程 / 浏览器交互?以确定性测试设置启动普通应用,对其运行 Playwright 或 Cypress。仅当浏览器流程必须加载 app-test 模块时,才使用 meteor test --full-app。

安装与脚本

meteor add meteortesting:mocha
meteor npm install --save-dev @types/mocha

package.json:

{
  "scripts": {
    "test": "TEST_WATCH=1 meteor test --driver-package meteortesting:mocha",
    "test:ci": "meteor test --once --driver-package meteortesting:mocha",
    "test:full": "meteor test --full-app --driver-package meteortesting:mocha"
  }
}

测试模式约定

  • 普通测试模式会主动加载 .test[s]. 和 .spec[s]. 文件,普通应用代码仅在测试导入时加载。
  • 文件名发现会忽略 tests/ 目录。配置了 meteor.testModule 时,改为加载其显式入口模块及其导入。
  • --full-app 会主动加载普通应用以及 .app-test[s]. 和 .app-spec[s]. 文件,并设置 Meteor.isAppTest。
  • 针对 Meteor 3.5.2 Rspack full-app/TLA 行为、parent-private 发现失败,以及 Meteor 3.6-beta.0 修复的 jQuery 测试框架回归,请阅读特定版本的测试设置。应用版本特定修复前,先检查已解析的包和套件数量。
  • 要聚焦现有测试,优先使用受支持的驱动过滤器(如 MOCHA_GREP),先检查 .meteor/versions 中已安装的驱动;.only 仅临时使用。这些只选择已注册的测试,不会阻止其他测试模块被求值。如果加载本身造成干扰,检查 meteor.testModule,并在缩小入口导入前阅读 references/focused-runs.md。恢复所有临时聚焦或导入改动,然后运行正常的受影响套件。从项目读取已配置的入口路径;不要臆造路径、脚本、端口或辅助函数。
  • 针对 3.6-beta.3 Rspack 忽略异常或单独的旧版测试入口,使用架构和忽略边界。检查每个所选程序上预期的套件;现代零退出运行并不能证明旧版覆盖。

方法测试(服务器端)

import assert from "node:assert/strict";
import { Meteor } from "meteor/meteor";
import { Items } from "/imports/api/items";

if (Meteor.isServer) {
  describe("items.add", function () {
    beforeEach(async function () {
      await Items.removeAsync({});
    });

    it("inserts when authed", async function () {
      const _id = await Meteor.server.method_handlers["items.add"].apply(
        { userId: "u1" },
        [{ title: "x", qty: 1 }],
      );
      const doc = await Items.findOneAsync(_id);
      assert.equal(doc.title, "x");
    });

    it("rejects when unauthed", async function () {
      await assert.rejects(() =>
        Meteor.server.method_handlers["items.add"].apply({ userId: null }, [{}]),
      );
    });
  });
}

.apply(context, argsArray) 是文档化形式;Meteor.server.method_handlers[name] 是已注册的方法实现。传入带 userId 的桩 this(需要时还包括 unblock、connection 等)。

发布测试(服务器端)

if (Meteor.isServer) {
  describe("items.mine publication", function () {
    it("only ships the subscribing user's items", async function () {
      await Items.removeAsync({});
      await Items.insertAsync({ _id: "a", ownerId: "u1" });
      await Items.insertAsync({ _id: "b", ownerId: "u2" });

      const cursor = await Meteor.server.publish_handlers["items.mine"]
        .apply({ userId: "u1" }, []);
      const docs = await cursor.fetchAsync();
      assert.deepEqual(docs.map((d) => d._id), ["a"]);
    });
  });
}

对于使用底层 this.added / this.changed API 的发布,请通过 --full-app 模式用真实 DDP 客户端测试(见下一节)。

始终 await 直接发布处理器。常规处理器立即返回游标;异步处理器返回游标的 Promise。

端到端 DDP 测试(--full-app 模式)

import { DDP } from "meteor/ddp-client";
import { Mongo } from "meteor/mongo";
import { Tracker } from "meteor/tracker";

const conn = DDP.connect(Meteor.absoluteUrl());
const RemoteItems = new Mongo.Collection("items", { connection: conn });

function ready(sub) {
  return new Promise((resolve) => {
    const computation = Tracker.autorun((c) => {
      if (sub.ready()) {
        c.stop();
        resolve();
      }
    });
  });
}

const sub = conn.subscribe("items.mine");
await ready(sub);
const docs = RemoteItems.find().fetch();

使用 meteor test --full-app --driver-package meteortesting:mocha 运行。

客户端单元测试(Minimongo)

if (Meteor.isClient) {
  describe("Items minimongo", function () {
    beforeEach(function () { Items.remove({}); });

    it("filters by ownerId", function () {
      Items.insert({ _id: "a", ownerId: "u1" });
      Items.insert({ _id: "b", ownerId: "u2" });
      assert.equal(Items.find({ ownerId: "u1" }).count(), 1);
    });
  });
}

客户端测试在驱动启动的浏览器中运行。设置 TEST_BROWSER_DRIVER=puppeteer 时,浏览器为无头 Chromium,结果回传到服务器控制台。

不要把仅服务器端通过视为完整的客户端覆盖。没有浏览器连接时驱动会打印消息;在 CI 中配置 TEST_BROWSER_DRIVER 并确认客户端测试数量存在。

浏览器 E2E

对普通应用运行 Playwright 或 Cypress,除非测试需要 Meteor 的 app-test 模块。服务器命令必须能从干净克隆运行,使用受跟踪的非机密设置 fixture 或完整记录的测试环境变量。不要指向被忽略的开发者设置文件。

至少覆盖一个完整变更流程和一个被拒绝的服务器操作。基本页面标题检查不会检验异步方法桩、授权或发布就绪状态。

反模式

  • 使用 Jest。Jest 不理解 Meteor 的构建系统。使用 meteortesting:mocha。
  • Mock Mongo。对干净数据库运行真实驱动。
  • 忘记 if (Meteor.isServer) { ... } 守卫。服务器测试在浏览器测试框架中运行会崩溃。
  • 跳过 beforeEach 清理。测试会在多次运行间泄漏状态。
  • 捕获并记录 beforeEach 失败而不重新抛出。测试随后在未知状态下运行,可能报告误导性断言。
  • 对被拒绝的方法只断言数据库后置条件。还应要求 Promise 以预期的 Meteor.Error 拒绝。
  • 替换 Meteor.userId 或其他全局变量后不在 afterEach 中恢复。
  • 在异步测试代码中使用带回调的 Meteor.call。使用 Meteor.callAsync 或直接解包 method_handlers。

另见

  • references/mocha-setup.md
  • references/focused-runs.md
  • references/ddp-test-helpers.md
  • references/eval-cases.md