meteor-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Meteor 3 apps
Meteor 3 应用测试
meteortesting:mochaIf an existing test fails, hangs, or flakes and the failing layer is unknown,
use to isolate the cause. Return here when evidence points to
test setup, design, driver behavior, fixtures, or regression coverage.
meteor-debuggingmeteortesting:mocha如果现有测试出现失败、挂起或不稳定的情况,且未知失败层级,请使用来定位原因。当证据指向测试设置、设计、驱动程序行为、测试夹具或回归覆盖范围时,再回到此技能。
meteor-debuggingDecision flow
决策流程
- Unknown failure in an existing test? Use to classify application, data, harness, browser, environment, or shared-state causes.
meteor-debugging - Pure logic, no Meteor APIs? Plain Mocha or any test runner; nothing Meteor-specific.
- Method or publication? Integration test it with using
meteortesting:mochaorMeteor.server.method_handlers[name].apply(ctx, args).publish_handlers - Need to drive a real DDP client (cross-process or end-to-end DDP
semantics)? Use mode and
--full-app.DDP.connect - UI flow / browser interaction? Start the normal app with deterministic
test settings and run Playwright or Cypress against it. Use
only when the browser flow must load app-test modules.
meteor test --full-app
- 现有测试出现未知失败?使用来归类应用程序、数据、测试工具、浏览器、环境或共享状态等方面的原因。
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进行测试。仅当浏览器流程必须加载应用测试模块时,才使用。
meteor test --full-app
Setup
设置
bash
meteor add meteortesting:mocha
meteor npm install --save-dev @types/mochajson
// 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-mode conventions:
- Normal test mode eagerly loads and
*.test[s].*files, while ordinary application code loads only when imported by a test.*.spec[s].* - Filename discovery ignores directories. A configured
tests/instead loads its explicit entry module and imports.meteor.testModule - eagerly loads the normal app plus
--full-appand*.app-test[s].*files, and sets*.app-spec[s].*.Meteor.isAppTest
To focus an existing test, prefer a supported driver filter such as
after checking the installed driver in ; use
only temporarily. These select registered tests but do not prevent
other test modules from evaluating. If loading itself causes interference,
inspect and read before
narrowing entrypoint imports. Restore every temporary focus or import change,
then run the normal affected suite. Read the configured entrypoint paths from
the project; do not invent paths, scripts, ports, or helpers.
MOCHA_GREP.meteor/versions.onlymeteor.testModulereferences/focused-runs.mdbash
meteor add meteortesting:mocha
meteor npm install --save-dev @types/mochajson
// 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/versionsMOCHA_GREP.onlymeteor.testModulereferences/focused-runs.mdMethod test (server-side)
方法测试(服务端)
javascript
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]thisuserIdunblockconnectionjavascript
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]userIdthisunblockconnectionPublication test (server-side)
发布测试(服务端)
javascript
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"]);
});
});
}For publications using the low-level / API,
test with a real DDP client via mode (next section).
this.addedthis.changed--full-appAlways await the direct publish handler. Conventional handlers return the
cursor immediately; async handlers return a Promise of the cursor.
javascript
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"]);
});
});
}对于使用底层/ API的发布,请通过模式使用真实的DDP客户端进行测试(见下一节)。
务必直接等待发布处理程序。常规处理程序会立即返回游标;异步处理程序会返回游标Promise。
this.addedthis.changed--full-appEnd-to-end DDP test (--full-app
mode)
--full-app端到端DDP测试(--full-app
模式)
--full-appjavascript
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();Run with .
meteor test --full-app --driver-package meteortesting:mochajavascript
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:mochaClient unit test (Minimongo)
客户端单元测试(Minimongo)
javascript
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);
});
});
}Client tests run in the browser the driver spawns. With
, the browser is headless Chromium and
results flow back to the server console.
TEST_BROWSER_DRIVER=puppeteerDo not treat a server-only green run as complete client coverage. The driver
prints a message when no browser is connected; configure
in CI and confirm client test counts are present.
TEST_BROWSER_DRIVERjavascript
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);
});
});
}客户端测试在驱动程序启动的浏览器中运行。当设置时,浏览器为无头Chromium,测试结果会返回至服务器控制台。
不要将仅服务端的测试通过视为完整的客户端覆盖。当没有浏览器连接时,驱动程序会打印提示信息;请在CI中配置并确认客户端测试计数存在。
TEST_BROWSER_DRIVER=puppeteerTEST_BROWSER_DRIVERBrowser E2E
浏览器端到端测试
Run Playwright or Cypress against the normal application unless the test
needs Meteor's app-test modules. The server command must work from a clean
clone with a tracked, nonsecret settings fixture or fully documented test
environment variables. Do not point it at an ignored developer settings file.
Cover at least one complete mutation flow and one rejected server action.
Basic page-title checks do not exercise async method stubs, authorization, or
publication readiness.
除非测试需要Meteor的应用测试模块,否则请针对常规应用运行Playwright或Cypress。服务器命令必须能在干净的克隆项目中运行,使用已跟踪的非保密设置夹具或文档齐全的测试环境变量。不要指向被忽略的开发者设置文件。
至少覆盖一个完整的变更流程和一个被拒绝的服务器操作。基础的页面标题检查无法验证异步方法存根、授权或发布就绪状态。
Anti-patterns
反模式
- Reach for Jest. Jest does not understand Meteor's build system. Use
.
meteortesting:mocha - Mock Mongo. Run the real driver against a clean DB.
- Forget guards. Server tests crash if they run in the browser harness.
if (Meteor.isServer) { ... } - Skip cleanup. Tests leak state across runs.
beforeEach - Catch and log a failure without rethrowing. The test then runs against unknown state and can report a misleading assertion.
beforeEach - Assert only the database postcondition for a rejected method. Also require
the Promise to reject with the expected .
Meteor.Error - Replace or another global without restoring it in
Meteor.userId.afterEach - Use with callbacks in async test code. Use
Meteor.callor unwrapMeteor.callAsyncdirectly.method_handlers
- 使用Jest。Jest无法理解Meteor的构建系统,请使用。
meteortesting:mocha - 模拟Mongo。请针对干净的数据库运行真实驱动程序。
- 忘记添加防护。服务端测试若在浏览器工具中运行会崩溃。
if (Meteor.isServer) { ... } - 跳过清理步骤。测试会在多次运行间泄露状态。
beforeEach - 捕获并记录的失败但不重新抛出异常。测试会在未知状态下运行,可能产生误导性的断言结果。
beforeEach - 仅断言被拒绝方法的数据库后置条件。还需验证Promise是否抛出了预期的。
Meteor.Error - 替换或其他全局变量但未在
Meteor.userId中恢复。afterEach - 在异步测试代码中使用带回调的。请使用
Meteor.call或直接调用Meteor.callAsync。method_handlers
See also
另请参阅
references/mocha-setup.mdreferences/focused-runs.mdreferences/ddp-test-helpers.mdreferences/eval-cases.md
references/mocha-setup.mdreferences/focused-runs.mdreferences/ddp-test-helpers.mdreferences/eval-cases.md