@firebase-bridge/firestore-admin (英语)
高度忠诚 模拟游戏 联 合 国 Firestore 管理员 SDK用于快速、确定性后端单元测试的目的(没有模拟器启动装置,没有部署回路).
这是什么
这个软件包让你运行一个真正的 firebase-admin 消防车 实例 正在处理 相对于一个高纯度生产Firestore语义(CRUD、批量、交易、查询、汇总、矢量值、听众等)的内存数据库.
- 仅后端 (节点.js). 无客户端/浏览器 API.
- 发展依赖性 打算用于 测试 财务报告和审定财务报表 快速原型 后端逻辑.
- 将起火点功能 v1/v2 起火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点火点 @ firebase-bridge/firestore-函数。 。 。 .
重要数字 :
@firebase-bridge/firestore-admin模拟数据库,而不是管理员SDK本身. 当你使用它时,你正在和真正的Firestore Admin SDK实例互动 — — 和生产中使用的一样。 模拟器只是修补它自己的Google API ClientsDK在盖子下执行, 这意味着你的测试在没有仿真器和网络呼叫的情况下,进行真实的Admin SDK行为.
什么时候用
- 使用后端代码的单元和集成测试
firebase-admin/firestore - CI 现场 Firestore 模拟器 缓慢或无法使用
- 需要可控时钟和快速重排的决定性测试
为什么不是仿真器(用于此用途)
- 没有时间了 零部署环. 0 外部进程——只是编辑、保存和测试
- 决定性 模拟火炉 有可控制的时间
- 适合紧凑的测试循环和CI,其中启动成本很重要
配套软件包
- 将起火点功能(v1 & v2) Firestore 触发器绑定在一个模拟起火点数据库中 @ firebase-bridge/firestore-函数。 。 。 .
- 对于一个高的忠诚 模拟引用层 页:1 Firebase HTTPS 云函数 (v1 & v2) — 运行真实
onCall编号 :onRequest本地处理者, 符合实际 认证, (中文(简体) ), 应用检查, (中文(简体) ), 实例标识,和页眉(没有仿真符)——使用相伴包 @firebase-bridge/auth-context (英语)。 。 。 .
支助
本项目在《公约》下免费提供 Apache 2.0 许可证。 。 。 .
如果您认为它有用并愿意支持正在进行的开发,你可以 给我买杯咖啡。 。 。
安装
# npm
npm i -D @firebase-bridge/firestore-admin firebase-admin
# pnpm
pnpm add -D @firebase-bridge/firestore-admin firebase-admin
# yarn
yarn add -D @firebase-bridge/firestore-admin firebase-admin
端点 :
firebase-admin二. 支助 节点 : • 建议18+ {\FN华文楷体\FS16\1CHE0E0E0}TS: 严格模式推荐.
快速启动( Jest/ Vites)
以下列出两种常见的设置样式. 更倾向于快速重置方法 用于速度; 使用 每一次测试新鲜 DB 当您需要完整的 DB 生命周期隔离时.
快速重置(首选)
为整个套间建立一个环境和一个数据库; 测试间重置数字 :
import { Firestore } from 'firebase-admin/firestore';
import { FirestoreMock } from '@firebase-bridge/firestore-admin';
describe('Example suite (fast resets)', () => {
const env = new FirestoreMock();
const firestore: Firestore = env.createDatabase().firestore();
afterEach(() => {
env.resetAll(); // clears all databases in this env, keeps them alive
});
it('writes and reads', async () => {
const ref = firestore.collection('users').doc('ada');
await ref.set({ name: 'Ada', score: 1 });
const snap = await ref.get();
expect(snap.exists).toBe(true);
expect(snap.data()).toEqual({ name: 'Ada', score: 1 });
});
});
B) 新鲜DB(完全隔离)
每个测试提供一个新的逻辑 DB 删除 在每个之后 :
import { Firestore } from 'firebase-admin/firestore';
import { FirestoreMock } from '@firebase-bridge/firestore-admin';
describe('Example suite (fresh DB per test)', () => {
const env = new FirestoreMock();
let firestore!: Firestore;
beforeEach(() => {
firestore = env.createDatabase().firestore();
});
afterEach(() => {
env.deleteAll(); // disposes all databases in this env
});
it('writes and reads', async () => {
const ref = firestore.collection('users').doc('ada');
await ref.set({ name: 'Ada', score: 1 });
const snap = await ref.get();
expect(snap.exists).toBe(true);
expect(snap.data()).toEqual({ name: 'Ada', score: 1 });
});
});
多个数据库和 Firestore 隔离
您可以主机 许多逻辑数据库 在一个环境中,你可以创建 多个 Firestore 实例 页:1 不变 当您需要Firestore 级别隔离时(例如,独立的听众),该数据库将显示共享状态.
const env = new FirestoreMock();
// Two separate logical databases
const dbA = env.createDatabase('proj-A', '(default)');
const dbB = env.createDatabase('proj-B', '(default)');
const fsA1 = dbA.firestore();
const fsA2 = dbA.firestore(); // isolated Firestore instances, same DB
const fsB = dbB.firestore(); // different DB entirely
核心概念和 API
这个包裹暴露出一小组高杠杆原始人. 以下名称为 实际出口。 。 。 .
class FirestoreMock
顶级 环境 拥有一个或多个内存数据库和一个可控制的时钟.
-
createDatabase(options?: FirestoreControllerOptions): FirestoreController提供使用选项对象的新数据库。 所有字段都是可选的,但有默认值:projectId默认为"default-project"databaseId默认为"(default)"location默认为"nam5"namespace默认为"(default)"如果您在测试中需要指定位置或命名空间,请使用此表单.
-
createDatabase(projectId?: string, databaseId?: string): FirestoreController通过明确的ID提供一个新的数据库(如上所述默认). -
getDatabase(projectId?: string, databaseId?: string): FirestoreController访问已存在的数据库( 如果缺少/ 删除则扔出) . -
databaseExists(projectId?: string, databaseId?: string): boolean -
deleteAll(): void- 删除环境中的所有数据库. -
resetAll(): void– 重置所有数据库(data & stats)而无需删除. -
systemTime: SystemTime– 说吧 – 可控制的时间来源 用于确定性测试.
class FirestoreController
一个把柄到一个 单一逻辑数据库 (单位:千美元) projectId 财务报告和审定财务报表 databaseId) (中文(简体) ).
-
projectId: string, (中文(简体) ),databaseId: string -
location: string– Firestore 数据库位置标识符用于 CloudEvents 和资源元数据. 接受多区域ID(例如:.nam5, (中文(简体) ),eur3)或区域身份证(例如.us-central1) (中文(简体) ). 默认为nam5如果省略,则删除. -
namespace: string– 数据存储取名空间. 对于 Firestore 本地模式,应该保留(default)。用于模拟数据存储器事件时的忠诚。 默认为(default)如果省略,则删除. -
firestore(settings?: Settings): Firestore创建一个 Firestore 管理员 SDK 实例 范围 到此数据库. -
exists(): boolean- 数据库是否仍然存在. -
epoch(): number- 单调地增加数据库的时代版本(逐一递减)reset()) (中文(简体) ). -
version(): number- 数据库中单调增加的原子承诺版本. -
delete(): void- 删除此数据库;后续调用(除此之外)exists()编号 :reset())掷出. -
reset(): void- 清除文档和数据,但保持 DB 存活. -
getStats(): FirestoreMockStats– 当前累积数据快照. -
watchStats(watcher: (s: FirestoreMockStats) => void): () => void– 订阅 stat 更改( 返回未订阅) . -
watchLifecycle(watcher: (s: DatabaseLifecycleEventArg) => void): () => void- 订阅生命周期事件(返回未订阅者). -
database: DatabaseDirect- 直接/低级访问内存数据库(见下文).
class DatabaseDirect
薄而同步的外观 直接数据访问 (种子、检查、结构进出口)。 它绕过行政SDK对象,但保持了Firestore语义学.
-
检查和结构
listCollectionIds(documentPath: string): string[]listDocuments(collectionPath: string, showMissing: boolean): MetaDocument[]query<T>(q: DocumentQuery<T>): MetaDocumentExists<T>[]toStructuralDatabase(): StructuralDatabasefromStructuralDatabase(src: StructuralDatabase, merge?: MergeGranularity): NormalizedWriteResult- 转换助手 :
toMetaArray(), (中文(简体) ),toMetaMap(), (中文(简体) ),toMap(), (中文(简体) ),toDocumentPaths()
-
单一文件操作
getDocument<T>(path: string): MetaDocument<T>setDocument<T>(path: string, data: T): MetaDocument<T>deleteDocument<T>(path: string): MetaDocument<T>
-
批量操作 (原子)
batchSet<T>(...docs: DatabaseDocument<T>[]): MetaDocument<T>[]batchDelete<T>(...paths: string[]): MetaDocument<T>[]batchWrite<T>(writes: (DatabaseDocument<T> | string)[]): MetaDocument<T>[](单位:千美元){ path, data }→set· ;string→delete(中文(简体) )
触发 : 低等触发登记在
DatabaseDirect这在白盒测试中是有用的。 对于云函数等同(v1/v2事件,主题,元数据),偏好 @ firebase-bridge/firestore-函数 同伴包裹...
低等触发实例( DatabaseDirect)
import { FirestoreMock } from '@firebase-bridge/firestore-admin';
const env = new FirestoreMock();
const ctl = env.createDatabase('proj', '(default)');
const direct = ctl.database;
const unsubscribe = direct.registerTrigger({
route: 'users/{uid}/posts/{pid}',
callback: ({ params, doc }) => {
// Derive semantic kind from lineage
const prev = doc.previous;
const kind =
!prev?.exists && doc.exists
? 'create'
: prev?.exists && doc.exists
? 'update'
: prev?.exists && !doc.exists
? 'delete'
: 'write';
console.log(`[${kind}] users/${params.uid}/posts/${params.pid}`, {
exists: doc.exists,
version: doc.version,
updateTime: doc.updateTime.toDate().toISOString(),
});
// doc.data is deeply frozen; use cloneData() for a mutable copy
const mutable = doc.cloneData();
// ...assertions, enqueue side effects for tests, etc.
},
});
// Perform writes via Firestore or DatabaseDirect
const fs = ctl.firestore();
await fs
.collection('users')
.doc('u1')
.collection('posts')
.doc('p1')
.set({ title: 'hello' });
unsubscribe();
注释:
- 触发起火 之后 每一次原子实验.
- 如果多个写到同一文档的单项承诺,则仅指 最终状态 因为那条路已经实现.
- 对于云函数忠诚度(v1/v2有效载荷塑造、对象、事件ID),偏好 @ firebase-bridge/firestore-函数 配套软件包.
数据库指令——写作(包括 fromStructuralDatabase() 形状)
import {
FirestoreMock,
StructuralDatabase,
MergeGranularity,
} from '@firebase-bridge/firestore-admin';
const env = new FirestoreMock();
const ctl = env.createDatabase('proj', '(default)');
const direct = ctl.database;
// 1) Single set / delete
direct.setDocument('users/alice', { name: 'Alice', score: 7 });
direct.deleteDocument('users/bob'); // no-op if it didn't exist
// 2) Batch set / delete (atomic)
direct.batchSet(
{ path: 'users/bob', data: { name: 'Bob', score: 3 } },
{ path: 'users/cara', data: { name: 'Cara', tags: ['pro'] } }
);
direct.batchDelete('users/bob', 'users/ghost');
// 3) Heterogeneous batch write (atomic)
// - {path,data} → normalized to 'set' (merge: 'root')
// - 'string' → normalized to 'delete'
direct.batchWrite([{ path: 'users/alice', data: { score: 8 } }, 'users/ghost']);
// 4) Import from a structural snapshot
const snapshot: StructuralDatabase = {
users: {
alice: {
data: { name: 'Alice', role: 'admin' },
collections: {
posts: {
p1: { data: { title: 'Hello', likes: 1 } },
p2: { data: { title: 'Second', likes: 0 } },
},
},
},
cara: { data: { name: 'Cara' } },
},
products: {
p1: { data: { sku: 'ABC-123', price: 19.95 } },
},
};
// Merge strategies:
// - 'root' → replace entire doc with provided data
// - 'branch' → deep-merge maps; scalars/arrays replace
// - 'node' → apply only explicit field paths (like update/mergeFields)
direct.fromStructuralDatabase(snapshot, 'root' satisfies MergeGranularity);
// 5) Export to a structural snapshot (round-trippable)
const roundTrip = direct.toStructuralDatabase();
fromStructuralDatabase返回正则写入结果(服务器时间+MetaDocument[]) (中文(简体) ). 所有写作助手都是 原子 在批量调用边界.
数据库指令 - stats() (例为外形
import { DatabaseStats } from '@firebase-bridge/firestore-admin';
const s: DatabaseStats = direct.stats();
console.log(s);
/*
{
// Operation counters (cumulative until reset)
writes: 4,
reads: 3,
deletes: 1,
noopReads: 1,
noopWrites: 0,
noopDeletes: 1,
// Structural counters (current view of the tree)
documentCount: 3,
collectionCount: 2,
stubDocumentCount: 0,
stubCollectionCount: 0
}
*/
- 使用
ctl.getStats()用于控制范围统计(添加 DB 身份). - 重置计数器 财务报告和审定财务报表 在保存数据库时擦除所有数据, 调用
ctl.reset()(单 DB) 或env.resetAll()(所有DB). 这个 删除所有文档和收藏, (中文(简体) ), 在不引用监视器回调的情况下冲出待定的更改, (中文(简体) ), 0 全部数据库数据,以及 将内部更改版本非ce重置为0。 。 。 .
数据库指令 - query() 实例
DocumentQuery<T> 提供范围界定(根/文档、收藏ID、收藏组切换),可选时间点 readTime,则a predicate 超过现有文件.
import { Timestamp } from 'firebase-admin/firestore';
import {
DocumentQuery,
MetaDocumentExists,
} from '@firebase-bridge/firestore-admin';
// 1) All docs in top-level 'users'
const q1: DocumentQuery = {
parent: '',
allDescendants: false,
collectionId: 'users',
predicate: () => true,
};
const users: MetaDocumentExists[] = direct.query(q1);
// 2) Collection group query: any 'posts' anywhere under 'users/alice'
const q2: DocumentQuery<{ title: string; likes: number }> = {
parent: 'users/alice',
allDescendants: true,
collectionId: 'posts',
predicate: (m) => (m.data.likes ?? 0) >= 1,
};
const hotPosts = direct.query(q2);
// 3) As-of read (point-in-time)
const at: Timestamp = Timestamp.fromMillis(Date.now() - 1000);
const q3: DocumentQuery = {
parent: '',
allDescendants: true,
predicate: () => true,
readTime: at, // include docs with updateTime <= at
};
const asOfDocs = direct.query(q3);
结果为:
MetaDocumentExists<T>[]只有(不包括没有的文件)。 在测试代码中按需要排列.
MetaDocumentExists 打开 MetaDocumentNotExists (你得到的东西)
从直接操作、查询和写结果中返回的不可移动快照.
共同领域MetaDocument<T>(中文(简体) )
epoch: number- 内部数据库 时代 每次重置内部数据库时递增parent: string- 医生父母的收集路径path: string- 完全合格的文档路径id: string- 最后一个部分pathserverTime: Timestamp- 制作行动的权威承诺/适用时间updateTime: Timestamp— doc 的上个更新时间( 如果从未存在过 0)version: number- 单调增加的原子 承诺 数据库版本hasChanges: boolean- 生产操作是否改变了文件createTime?: Timestamp— 当 doc 首次创建时( 如果从未存在, 未定义)data?: T– 说吧 – 深冻了 数据; 使用cloneData()可变副本previous?: MetaDocument<T>– 说吧 – 即时 同一路径的上一个状态(在hasChanges === true; 不是一个中转链)cloneData(): T | undefined- 防御性深克隆data
改进
-
MetaDocumentExists<T>数字 :exists: truecreateTime: Timestampdata: T
-
MetaDocumentNotExists<T>数字 :exists: falsecreateTime?: undefineddata?: undefined
const m = direct.getDocument<{ name: string }>('users/alice');
if (m.exists) {
// MetaDocumentExists<{name:string}>
console.log(m.path, m.updateTime.toDate(), m.data.name);
const mutable = m.cloneData();
} else {
// MetaDocumentNotExists
console.log('missing:', m.path);
}
作为阵列和地图的快照( M)toMetaArray, (中文(简体) ), toMetaMap, (中文(简体) ), toMap(中文(简体) )
// Seed
direct.batchSet(
{ path: 'users/alice', data: { name: 'Alice', score: 7 } },
{ path: 'users/cara', data: { name: 'Cara', score: 3 } }
);
// 1) Array of existing meta documents (ordered)
const arr = direct.toMetaArray();
// 2) Map of path → MetaDocumentExists
const metaMap = direct.toMetaMap();
// 3) Map of path → plain document data (no meta)
const dataMap = direct.toMap();
class SystemTime
写作、变形和快照时间戳所观察到的“现在”决定控制.
now(): Date- 目前每个战略的时间.system(): void- 真正的时钟.constant(date: Date): void- 固定瞬间.offset(...): 从 a 开始 根号 时间和实时前进(超载支持)Date或者说UTC parts) (中文(简体) ).advance(msOrParts): void- 跳出时钟向前.custom(fn: () => Date): void- 完全定制时间生成器.
系统时间( M) Timestamp.now() 来了
在内部,这个模拟器来自所有承诺/写作/更新时间 SystemTime然而, firebase-admin’: Timestamp.now() 调用 真实时钟 (例如, Date.now())),我们这样做 没有 猴-patch默认。 因此,如果您的应用程序/测试代码 直接 电话 Timestamp.now(),它不会反映 SystemTime 除非您采取额外步骤.
选项 :
- 不要打电话
Timestamp.now()直接在测试. 相反,来自SystemTime数字 :
import { Timestamp } from 'firebase-admin/firestore';
const ts = Timestamp.fromDate(env.systemTime.now());
- 猴头
Timestamp.now测试期间 (范围可逆):
import { Timestamp } from 'firebase-admin/firestore';
let restore: undefined | (() => void);
beforeAll(() => {
const original = Timestamp.now;
(Timestamp as any).now = () => Timestamp.fromDate(env.systemTime.now());
restore = () => {
(Timestamp as any).now = original;
};
});
afterAll(() => restore?.());
- 使用您的测试选手的假工具 使全球时钟与
SystemTime。 。 。 .
- 欢乐 (现代假计时器):
import { jest } from '@jest/globals';
beforeAll(() => {
jest.useFakeTimers();
});
beforeEach(() => {
jest.setSystemTime(env.systemTime.now());
});
afterAll(() => {
jest.useRealTimers();
});
- 维特 (单位:千美元)
vi.useFakeTimers()编号 :vi.setSystemTime(),类似地.
我们故意避免补丁
Timestamp.now()自动保存此库的侧效果 。 全体 内部 时间戳(承诺时间,serverTimestamp, (中文(简体) ),updateTime, (中文(简体) ),writeTime做荣誉SystemTime。 。 。 .
结构快照(种子、diff、圆行)
这个 结构 类型允许您在不使用 Firestore 对象的情况下进行快取/导入数据库状态 :
StructuralDatabase– 收藏的对象树 – 文件 – 嵌入式收藏.StructuralCollection, (中文(简体) ),StructuralCollectionGroup, (中文(简体) ),StructuralDocument
import {
StructuralDatabase,
DatabaseDirect,
} from '@firebase-bridge/firestore-admin';
const ctl = new FirestoreMock().createDatabase('proj', '(default)');
const direct: DatabaseDirect = ctl.database;
// Export
const snapshot: StructuralDatabase = direct.toStructuralDatabase();
// Import/merge
const writes = direct.fromStructuralDatabase(
snapshot /*, 'root' | 'collection' | 'document' */
);
expect(writes.count).toBeGreaterThan(0);
状态和可观察性
使用 FirestoreController.getStats() 坚持忠诚和跟踪业务:
const ctl = new FirestoreMock().createDatabase();
const fs = ctl.firestore();
await fs.collection('c').doc('d').set({ a: 1 });
const stats = ctl.getStats();
expect(stats.documents.total).toBe(1);
在测试期间订阅实时更新 watchStats() (记得未订阅):
const stop = ctl.watchStats((s) => {
// e.g., console.log('writes', s.operations.writes.total)
});
// ...
stop();
关于忠诚的说明(高级)
- 原子:批发/交易为原子;变换结果遵循Firestore的订购规则.
- 时间数字 :
updateTime, (中文(简体) ),writeTime,并存储serverTimestamp遵循 Firestore 关系; 使用SystemTime做决定性测试. - 查询: 过滤器(包括
or编号 :not-in编号 :in、命令、光标、限制、集合(例如,count()) (中文(简体) ). - 矢量值: 支持
FieldValue.vector()字段和最近的“邻居”特征,通过行政API表面显示. - 听众: 文档和查询听众的行为像流出 API 有单调
readTime和适当的改变套. - 已分割的查询( Collection Group. get partities () / Query. get Partities () ~ GAPIC 分区查询): 目前被打出- 模拟返回一个空流( 无分区) , 用于调取结果的测试。 使用仿真器/火炬来进行真正的分隔语义(平行导出/批发).
如果你发现行为与真正的行政SDK或模拟器不同, 请用最小的重写打开一个问题.. 忠诚是项目第一优先。 。 。 .
一揽子出口(公共)
您可以从此软件包导入 :
FirestoreMock, (中文(简体) ),FirestoreControllerDatabaseDirect结构类型:StructuralDatabase, (中文(简体) ),StructuralCollection, (中文(简体) ),StructuralCollectionGroup, (中文(简体) ),StructuralDocument- 时间控制 :
SystemTime - 有用的断言类型:
MetaDocument, (中文(简体) ),MetaDocumentExists, (中文(简体) ),MetaDocumentNotExists, (中文(简体) ),MergeGranularity, (中文(简体) ),Trigger, (中文(简体) ),TriggerEventArg, (中文(简体) ),FirestoreMockStats
云函数: 在测试中登记/使用触发器,取决于 @ firebase-bridge/firestore-函数 配套软件包.
解决问题
-
我的Firestore电话不会被嘲笑 确保您使用
firestore = env.createDatabase(...).firestore()从aFirestoreController创建于 这种环境. -
我需要新鲜的状态 在测试之间 偏爱
env.resetAll()(快)结束env.deleteAll()(完全处理DB). 也可以通过controller.reset()。 。 。 . -
时间敏感的说法是片面的 固定或提前时间通过
env.systemTime。 。 。 . -
运行后测试是否完整退出
确保所有内部数据库在测试结束后得到明确处理.
添加一个afterEach()或者说afterAll()钩指以下之一,视范围而定:env.deleteAll()- 删除 所有数据库 输入FirestoreMock环境controller.delete()- 删除a 单一数据库 通过其FirestoreController
这保证了背景资源(计时器,间隔器,收听器等)的放出,并允许你的测试跑取器干净地关闭.
版本兼容性( C)
- 同伴依赖 :
firebase-admin(参见您的包裹)peerDependencies用于支持范围. - 18. TypeScript项目(ESM或CJS)是通过Admin SDK支持的.
捐款
谢谢你的关心! 这个项目在 最小维护模式。 。 。 .
- 问题第一. 请打开一个问题 一个明确的复验或失败测试。 非邀约特性公关可能关闭.
- 限于:bug通过测试修正,小的docs改进,或者建立/释放卫生. 新的特点要求首先在某一问题上提出可接受的建议.
- 需要进行测试. 变化必须包括高可靠性测试,显示与Firebase模拟器的对齐(或有文件记载的相差).
- 审取自性. 我分批审查,可能很慢 没有支持解放军.
- 范围护栏. 目标是忠于Firestore/Admin SDK语义;范围外的特性会被拒绝.
如果这对你有用的话,特别欢迎棒极了的bugfixs和docs tweaks.
许可证
Apache-2.0 2026 布莱斯·马歇尔
商标归属
这个项目是 没有 隶属于Google LLC,与其相关联,或得到其认可. “Firebase”和“Firestore”是Google有限责任公司的商标。 名称仅用于识别相容性,并不意味着认可.