@effect-firebase/mock

中文文档
1.0.1发布于 2026/10/08MITTypeScriptES Module构建来源已验证近 30 天下载 3108

An in-memory, reactive FirestoreService implementation for testing and developing Effect Firebase applications. No Firebase connection required.

原始使用文档
文档目录11 节
  1. Installation
  2. Usage
  3. Fixtures
  4. Simulated states
  5. Driving the backend from outside Effect
  6. Multiple repositories
  7. Error handling
  8. Stubbing individual methods
  9. NaN field values
  10. Limitations
  11. License

@effect-firebase/mock

An in-memory, reactive FirestoreService implementation for testing and developing Effect Firebase applications. No Firebase connection required.

Beyond a plain test double, the mock is a small simulated backend built for developer experience:

  • Fixtures — seed hard-coded models through your real schemas, so reads exercise the exact decoding path production data takes.
  • Reactive streams — streamDoc / streamQuery are live: writes and runtime toggles push new emissions through already-subscribed streams, just like onSnapshot.
  • Simulated states — flip any collection between data, empty, loading and error at runtime with the MockController, and watch your UI's spinner, empty and error paths render with no backend involved.
  • Latency simulation — add artificial delay to every operation.
  • Write fidelity — server timestamps materialize on write, delete/arrayUnion/arrayRemove/increment sentinels are honored, and queries (where, orderBy, cursors, limits, collection groups) are evaluated in-process.

Installation

npm install --save-dev @effect-firebase/mock

Usage

Provide layer in place of the real Admin or Client 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())),
);

Repositories built with Firestore.makeRepository are effects that only need FirestoreService, so providing the mock layer once covers all of them. Each Effect.provide(layer()) call gets a fresh in-memory store, so tests are isolated by default.

Fixtures

Seed the backend with hard-coded models. Documents are encoded through the model's schema, so getById, query and streams decode them exactly like real data:

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] });

For documents without a model schema, use rawFixture with already-encoded data:

import { rawFixture } from '@effect-firebase/mock';

const settings = rawFixture('settings', {
  general: { theme: 'dark' },
});

To fill a page with volume (long lists, pagination, layout stress), map over an array — fixture takes any ReadonlyArray of models:

const manyPosts = fixture(PostModel, {
  collectionPath: 'posts',
  idField: 'id',
  docs: Array.from({ length: 50 }, (_, i) => makePost(i)),
});

Simulated states

The layer also provides a MockController service for driving the backend at runtime — from tests, a dev panel, or a devtools plugin:

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 also exports data, empty and loading constants; MockState.error takes a Firestore error code or a full FirestoreError. Fixtures passed to layer/make and controller.seed must use models whose schemas need no extra encoding services.

States can also be set up front:

const mock = layer({
  fixtures: [posts],
  states: { comments: 'loading' },
  latency: '200 millis',
});

A collection group query (queryGroup / streamQueryGroup, or a repository's group view) resolves its state by collection ID, so states: { comments: 'loading' } covers both a top-level comments collection and the comments group across every parent. As in Firestore, a __name__ cursor (Query.orderByDocumentId) in a group query must be a full document path (posts/p1/comments/c1); a bare ID fails with invalid-argument, as it does on the real SDKs. A single-collection query takes a bare ID.

Driving the backend from outside Effect

make() returns a handle instead of just a layer: the same options as layer(), plus direct access to the controller as a plain value. Every controller effect requires no services, so React components, Storybook decorators or test helpers can run them with Effect.runPromise directly. This is what the @effect-firebase/devtools panel builds on:

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'));

Notes on semantics:

  • empty affects reads only; writes still land in the store.
  • loading suspends reads and writes, and live streams stop emitting. A stream subscribed while loading emits nothing until the state flips.
  • error fails effects per call. A live stream fails terminally (matching onSnapshot semantics) — consumers must re-subscribe after the state recovers, e.g. by refreshing the atom/query that owns the stream.

Multiple repositories

Provide one mock layer with fixtures for every collection; each repository reads from the same store:

const program = Effect.gen(function* () {
  const posts = yield* PostRepository;
  const users = yield* UserRepository;
  // ...
}).pipe(Effect.provide(layer({ fixtures: [posts, users] })));

Error handling

A missing document is Option.none(), not a failure. Use the error state to exercise failure paths:

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'
  ),
);

Stubbing individual methods

For a plain test double without the in-memory store, MockFirestoreService(overrides) returns a FirestoreService layer built from the methods you pass; any method you leave out throws "not implemented" when called.

NaN field values

Firestore stores and normalizes NaN, and the mock follows its semantics:

  • Ordering — NaN is placed in the total order below -Infinity, so orderBy lists it first ascending and last descending. Timestamp and GeoPoint sub-fields use the same rule for their numeric components.
  • Equality (==, !=) — NaN is equal only to NaN. This also drives arrayUnion / arrayRemove dedup, so a stored NaN neither absorbs nor is absorbed by a finite value.
  • Range filters (<, <=, >, >=) — a NaN field value is excluded from range scans entirely, even though it sorts below -Infinity for orderBy.
  • in — never matches a NaN field value, even with a NaN candidate (unlike == NaN, which matches).
  • not-in — always includes a NaN field value, regardless of the candidate list.
  • Live streams — streamDoc / streamQuery detect a NaN-to-number transition as a real change (a stored NaN and a finite number are not equal) and re-emit, instead of collapsing it via the old NaN-equals-everything comparison.

Limitations

  • In-memory only — no persistence between process restarts
  • Queries are evaluated in-process — behaviour may differ from real Firestore for edge cases (composite index requirements are not enforced, not-in/!= null semantics are simplified, and a NaN query operand such as where('v', '<', NaN) is not validated — real Firestore rejects it with invalid-argument, the mock returns an empty/ordered result; only NaN field-value semantics are emulated)
  • Simulated states are keyed per collection path (or the '*' wildcard), not per query; collection group queries resolve their state by collection ID
  • No security rules evaluation
  • withTransaction and withBatch run the effect directly — no retries, no rollback, and no staged writes
  • No multi-client synchronization

For tests that need full Firestore semantics, use the Firebase Emulator Suite.

License

MIT

版本与发布记录

@effect-firebase/mock 共发布 40 个版本,首次发布于 2025/10/10;过去 12 个月发布了 39 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.0.1latest2026/10/081 个月前
1.0.02026/10/051 个月前
1.0.0-beta.9beta2026/09/231 个月前
1.0.0-beta.82026/09/211 个月前
1.0.0-beta.72026/09/141 个月前
1.0.0-beta.62026/09/112 个月前

关于 @effect-firebase/mock 的常见问题

@effect-firebase/mock 是什么?

An in-memory, reactive FirestoreService implementation for testing and developing Effect Firebase applications. No Firebase connection required.

如何安装 @effect-firebase/mock?

在项目目录运行 npm install @effect-firebase/mock,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(^4.0.0)、effect-firebase(1.0.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effect-firebase/mock 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effect-firebase/mock 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effect-firebase/mock 可以商用吗?

@effect-firebase/mock 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effect-firebase/mock 的下载量有多少?

根据 npm 官方下载统计,@effect-firebase/mock 在 2026/10/13 – 2026/11/11 这 30 天内被下载 3,108 次,日均约 104 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effect-firebase/mock 还在维护吗?

最新版本 1.0.1 发布于 2026/10/08(1 个月前);至今共发布 40 个版本,其中过去 12 个月发布了 39 个;首次发布于 2025/10/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。