@ effect- firebase/ mock (英语)
记忆中,反应中 FirestoreService 实施测试和开发效应防火基地应用。 不需要Firebase连接 .
模拟器是一个小的模拟后端 用于 开发者经验数字 :
- 固定设备 ——种子硬码模型通过您真正的计划,所以读取锻炼精确的解码路径生产数据.
- 反应流 —— 说
streamDoc编号 :streamQuery正在直播:写作和运行时间切换将新的排放推入已订阅的溪流,就像onSnapshot。 。 。 . - 模拟状态 ——翻转之间的任何收藏
data, (中文(简体) ),empty, (中文(简体) ),loading财务报告和审定财务报表error运行时间MockController,并注意您的 UI 旋转器,空出和出错路径渲染而不涉及后端 . - 延迟模拟 ——为每次行动增加人为延迟.
- 写忠诚 ——服务器时间戳在写作时实现,
delete编号 :arrayUnion编号 :arrayRemove编号 :increment哨兵受到尊重,询问(在哪里、命令By、光标、限制、收集组)在进程中得到评价.
安装
npm install --save-dev @effect-firebase/mock
使用量
提供 layer 替换真正的管理员或客户端层:
import { Effect, Option } from 'effect';
import { layer as mockFirestore } from '@effect-firebase/mock';
await Effect.runPromise(
Effect.gen(function* () {
const repo = yield* PostRepository;
const postId = yield* repo.add({
title: 'Test',
content: '...',
status: 'draft',
});
const post = yield* repo.getById(postId);
expect(Option.getOrThrow(post).title).toBe('Test');
}).pipe(Effect.provide(mockFirestore())),
);
以 Firestore.makeRepository 仅需要的效果 FirestoreService,所以提供模拟层一次覆盖所有它们。 每个 Effect.provide(layer()) 呼叫得到一个新鲜的内存存储,所以测试默认是孤立的.
固定设备
用硬码模型来种子后端. 文件通过模型的图案编码,所以 getById, (中文(简体) ), query 和流解码 完全像真实数据:
import { fixture, layer } from '@effect-firebase/mock';
import { DateTime } from 'effect';
const posts = fixture(PostModel, {
collectionPath: 'posts',
idField: 'id',
docs: [
new PostModel({
id: PostId.make('1'),
title: 'Hello world',
content: '...',
createdAt: DateTime.makeUnsafe('2024-01-01'),
// ...
}),
],
});
const mock = layer({ fixtures: [posts] });
对于没有模型设计的文档,请使用 rawFixture 已编码数据 :
import { rawFixture } from '@effect-firebase/mock';
const settings = rawFixture('settings', {
general: { theme: 'dark' },
});
要填入一页的音量(长列表,平面图,排版压力),则要填入一个数组上的地图—— fixture 使用任意 ReadonlyArray 模型类型:
const manyPosts = fixture(PostModel, {
collectionPath: 'posts',
idField: 'id',
docs: Array.from({ length: 50 }, (_, i) => makePost(i)),
});
模拟状态
该层还提供了 MockController 在运行时驱动后端的服务—— 从测试、 dev 面板或 devtools 插件:
import { layer, MockController, MockState } from '@effect-firebase/mock';
Effect.gen(function* () {
const controller = yield* MockController;
// Live streams re-emit immediately:
yield* controller.setState('posts', 'empty');
yield* controller.setState('posts', 'loading'); // reads hang, streams go silent
yield* controller.setState('posts', 'error'); // reads/writes fail: code 'unavailable'
yield* controller.setState('posts', MockState.error('permission-denied'));
yield* controller.setState('posts', 'data'); // back to normal
// Apply to every collection at once:
yield* controller.setState(MockState.All, 'loading');
// Other controls:
yield* controller.clearState('posts'); // drop a per-collection override
yield* controller.setLatency('300 millis');
yield* controller.seed(morePosts);
yield* controller.setDoc('posts/raw', { title: 'Raw' }); // bypasses states and latency
yield* controller.removeDoc('posts/raw');
yield* controller.reset;
// Inspect:
const states = yield* controller.states;
const docs = yield* controller.docs; // all docs keyed by full path
// controller.changes is a Stream<StoreSnapshot> of every store change
});
MockState 还出口 data, (中文(简体) ), empty 财务报告和审定财务报表 loading 常数; MockState.error 使用 Firestore 错误代码或全码 FirestoreError. 传递给 layer编号 :make 财务报告和审定财务报表 controller.seed 必须使用不需要额外编码服务的模型.
国家也可以设立在以下前线:
const mock = layer({
fixtures: [posts],
states: { comments: 'loading' },
latency: '200 millis',
});
收藏组查询( E)queryGroup 编号 : streamQueryGroup,或一个
存储器 group 视图)通过收藏ID解决状态,所以
states: { comments: 'loading' } 覆盖两个顶层 comments
收藏和目录 comments 每个家长的组合。 就像在Firestore一样,
(单位:千美元) __name__ 光标( r)Query.orderByDocumentId在分组查询中必须是
完整文档路径 (e)posts/p1/comments/c1); 空ID失败
invalid-argument和真正的SDK一样 单一收集查询
拿个身份证明.
从外部驱动后端
make() 返回一个控点,而不只是一层:与 layer(),加直接访问控制器作为纯值。 每个控制器效果都不需要服务, 因此 React 组件、 故事书装饰器或测试助手可以使用 Effect.runPromise 直接说 这就是 @effect-firebase/devtools 面板基于:
import { Effect } from 'effect';
import { Atom } from 'effect/reactivity';
import { make } from '@effect-firebase/mock';
const mock = make({ fixtures: [posts] });
// Provide mock.layer to your app runtime (all provides share one store)...
const runtime = Atom.runtime(mock.layer);
// ...and drive the same store from anywhere:
await Effect.runPromise(mock.controller.setState('posts', 'loading'));
关于语义的说明:
empty影响只读; 写仍然停留在商店中.loading暂停读取 财务报告和审定财务报表 写作,而活的溪流停止了排放。 在装入状态翻转前, 流已订阅 .error每个呼叫都失效 一个活流失败 最终 (比对)onSnapshot语义学)——消费者必须在状态恢复后重新订阅,例如通过刷新拥有溪流的原子/平流.
多个存储器
为每个收藏提供一个带有固定装置的模拟层;每个寄存器都从同一商店读取:
const program = Effect.gen(function* () {
const posts = yield* PostRepository;
const users = yield* UserRepository;
// ...
}).pipe(Effect.provide(layer({ fixtures: [posts, users] })));
处理错误
缺少的文件是 Option.none()不是失败 使用 error 状态以运行失败路径 :
await Effect.runPromise(
Effect.gen(function* () {
const controller = yield* MockController;
yield* controller.setState('posts', 'error');
const repo = yield* PostRepository;
return yield* repo.query(Query.limit(10));
}).pipe(
Effect.provide(layer()),
Effect.catchTag('FirestoreError', (e) => Effect.succeed(e.code)), // 'unavailable'
),
);
B. 个别方法
对于一个简单的测试双倍 没有内幕商店, MockFirestoreService(overrides) 返回 a FirestoreService 层由您通过的方法所构建; 任何您删除的方法在调用时都会丢出“ 未执行的” .
NaN 字段值
消防仓库和正规化 NaN,该模型沿用其语义:
- 订购 —— 说
NaN顺序如下:-Infinity这样orderBy列出第一个升起和最后一个降起.Timestamp财务报告和审定财务报表GeoPoint子字段对其数组使用相同的规则. - 平等 (单位:千美元)
==, (中文(简体) ),!=(b) ——NaN仅等于NaN。此驱动器也arrayUnion编号 :arrayRemove脱出,所以存储NaN既不能吸收,也不能被有限价值所吸收. - 范围过滤器 (单位:千美元)
<, (中文(简体) ),<=, (中文(简体) ),>, (中文(简体) ),>=)——aNaN字段值完全从区域扫描中排除,即使下面有排序-Infinity页:1orderBy。 。 。 . in- 从不匹配NaN字段值,即使带有NaN候选( 不同于== NaN,该匹配).not-in- 总是包括NaN字段值,不论候选名单.- 生活流 —— 说
streamDoc编号 :streamQuery检测aNaN- 作为实际变化向数字过渡(存储)NaN和重犯,而不加以毁灭NaN- 等同一切比较.
限制
- 只有记忆——重启进程之间没有持久性
- 查询在进程中得到评价——行为可能与边缘案例的真火炉不同(复合指数要求没有执行,
not-in编号 :!=单语义简化,aNaN查询 操作 例如where('v', '<', NaN)不被验证——真实的Firstore拒绝使用invalid-argument,模拟返回空/有序结果;仅NaN字段值语义被模拟) - 模拟状态在每个收集路径(或
'*'通配符),不是每个查询;收藏组查询通过收藏ID解决其状态 - 没有安保规则评价
withTransaction财务报告和审定财务报表withBatch直接运行效果——不重复、不回滚和不进行舞台写作- 没有多客户同步
对于需要完整Firestore语义的测试,请使用 Firebase 模拟器套件。 。 。 .
许可证
麻省理工学院