testing-strategy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Strategy
测试策略
Tests are the only correctness instrument you control at build time: shape the suite
to the code, not to a decades-old ratio, and make each test assert behaviour a build
either passes or fails. Applies to every test under and .
test/integration_test/Read the reference for the task at hand:
- — per-layer harness, imports, teardown, edge tables (pure core, in-memory Drift, headless Notifier, integration).
references/test-layers.md - — fakes-over-mocks, enum-driven fake state, seeded fuzz, independent oracles, absence-of-a-failure-class tests.
references/property-and-fakes.md - — file-level floors, the coverage-lies-upward fix, the suite-time budget, and the manual-pass handoff.
references/coverage-and-budget.md
Run and before a PR.
scripts/check_test_hygiene.shscripts/run_tests.shGolden, RTL, and a11y widget mechanics live in ; this
skill governs everything below the pixel.
widget-golden-and-a11y-testing测试是你在构建时唯一能够掌控的正确性工具:让测试套件随代码塑形,
而不是套用几十年前的固定比例,并让每个测试都断言一种构建要么通过、
要么失败的行为。适用于 和 下的每个测试。
test/integration_test/针对当前任务阅读参考文档:
- — 各层测试 harness、imports、teardown、边界表(纯 core、内存 Drift、无头 Notifier、integration)。
references/test-layers.md - — 优先 fake 而非 mock、enum 驱动的 fake state、带种子的 fuzz、独立 oracle,以及验证某类失败不会发生的测试。
references/property-and-fakes.md - — 文件级下限、
references/coverage-and-budget.md修复、测试套件时间预算,以及 manual pass 交接。coverage-lies-upward
提交 PR 前运行 和 。
scripts/check_test_hygiene.shscripts/run_tests.shGolden、RTL 和 a11y 的 widget 机制归 ;
本 skill 管辖像素层以下的所有内容。
widget-golden-and-a11y-testingNon-negotiable rules
不可协商的规则
- Shape the suite to the code, not the pyramid. The 70/20/10 numbers trace to a
2011 test-size heuristic (its author said they were "pulled out of a hat") and
never described Flutter's unit/widget/integration taxonomy. Test at the cheapest
tier that can assert the behaviour: anything expressible as is a unit or property test, never a
f(input) -> output. Driving pure logic through the widget tree is slower, flakier, and hides which layer broke.pumpWidget - Put business rules in a Flutter-free package and inject a . Domain math lives in pure Dart with zero Flutter/plugin/IO imports, tested with
Clock(notpackage:test). The one time type isflutter_test'spackage:clock— NEVERClock, never a bespokeDateTime.now(). PureClockServicereads the ambientcore, pinned in tests withclock.now(); Riverpod/feature code reads the injectedwithClock(Clock.fixed(t), …), overridden withclockProvider(seam owned byclockProvider.overrideWithValue(Clock.fixed(t))). The ban is structural: a pure package declares no Flutter SDK constraint.value-objects-money-and-units - Assert invariants, not just examples. Every conversion has a round-trip
test () and rounding goldens at half-way/boundary values; every universal claim is a seeded fuzz loop (
decode(encode(x)) == x) or property test, checked against an independent oracle — never the production function under test. Print the generated input infor (var seed = 0; seed < N; seed++)so a failure is its own minimal repro.reason:on integers only;==on doubles.closeTo(x, 1e-9) - Test the data layer against a real in-memory engine. Use
, never a mocked DAO — a mocked DAO proves nothing about SQL, constraints, indexes, or migrations. Mock repositories only above the data layer.
NativeDatabase.memory()and close streams synchronously so reactiveaddTearDown(db.close)streams do not leak "Timer still pending"..watch() - Prefer bare-fakes over mocks for code you own. A
implements(noclass FakeX implements Xsuperclass) makes an interface change a compile error, models the risk (state, not call-order) as a field, and doubles as the contract's documentation. ReservenoSuchMethodfor genuinely external dependencies; nevermocktail/mockito(codegen, no null-safety win).@GenerateMocks - for every custom type passed to
registerFallbackValue/any()incaptureAny(). mocktail throws at runtime, not compile time, on a missing fallback — this is the single most common mocktail failure.setUpAll - Drive Notifiers headlessly with . Override provider dependencies via
ProviderContainer; never pump a widget to test state. Assert on the exposedoverrideWith/state, drive actions throughAsyncValue, and dispose the container in teardown..notifier - Guard one end-to-end acceptance gate. A single realistic scenario asserted to
the exact expected result, plus the conservation invariant (parts sum to the
whole). It is the test that proves the pieces compose. Back it with a runtime
assert tripwire inside the primitive itself () — free in release, catches the bug the moment it happens.
assert(sum(result) == total) - Hold an unrecoverable-bug-files floor by diff-review, not a coverage gate.
There is no automated percentage gate — coverage is a published report
(owns that). Instead hold a 100% floor on the handful of files (migrations, the money/allocate primitive, the parser with wire-format traps) where a gap is silent data loss — enforced by reading the diff, not by counting lines. A directory or global percentage gate rewards vanity tests and exclusion churn. First fix the coverage-lies-upward gap:
ci-pipeline-and-gatesomits files no test imports, so the number overstates safety.flutter test --coverage - Never on an indefinite animation (splash, shimmer, spinner) — it hangs on a 10-minute timeout. Use timed
pumpAndSettle()withpump(Duration).fakeAsync - Keep the suite fast, and hand structurally-untestable paths to a manual pass. A suite that costs minutes gets skipped, and a skipped suite is a distrusted one. Anything an emulator cannot reproduce (real audio, OEM device diversity, native surfaces without a Flutter engine) is enumerated in a named manual pre-release pass, not faked green — a green test that proves nothing is worse than an admitted gap.
- 按代码塑形测试套件,而不是遵循金字塔。 70/20/10 这一比例可追溯至 2011 年的一项 test-size 启发法(其作者称这些数字是“随手编出来的”),且从未描述 Flutter 的 unit/widget/integration 分类体系。应在能够断言该行为的成本最低层级进行测试:任何可表达为 的逻辑都应使用 unit test 或 property test,绝不能通过
f(input) -> output驱动。通过 widget tree 驱动纯逻辑不仅更慢、更易波动,还会掩盖究竟是哪个层出现了问题。pumpWidget - 将业务规则放在不依赖 Flutter 的 package 中,并注入 。 领域计算应位于纯 Dart 中,不引入任何 Flutter/plugin/IO imports,并使用
Clock测试(而不是package:test)。唯一的时间类型是flutter_test的package:clock——绝不使用Clock,也不创建定制的DateTime.now()。纯ClockService读取环境中的core,测试中通过clock.now()固定时间;Riverpod/feature 代码读取注入的withClock(Clock.fixed(t), …),并通过clockProvider覆盖它(该 seam 由clockProvider.overrideWithValue(Clock.fixed(t))负责)。这是结构性禁令:纯 package 不得声明 Flutter SDK constraint。value-objects-money-and-units - 断言不变量,而不只是断言示例。 每种转换都必须有 round-trip 测试(),并在半程值和边界值上进行 rounding golden 测试;每项普适性断言都必须采用带种子的 fuzz 循环(
decode(encode(x)) == x)或 property test,并对照独立 oracle 检查——绝不能使用被测的生产函数本身。将生成的输入打印在for (var seed = 0; seed < N; seed++)中,使每次失败本身都能成为最小复现。整数仅使用reason:;double 使用==。closeTo(x, 1e-9) - 使用真实的内存引擎测试数据层。 使用 ,绝不使用 mocked DAO——mocked DAO 无法证明 SQL、constraints、indexes 或 migrations 的任何行为。只有在数据层之上才应 mock repositories。调用
NativeDatabase.memory(),并同步关闭 streams,避免响应式addTearDown(db.close)streams 泄漏 “Timer still pending”。.watch() - 对自己拥有的代码,优先使用 bare-fake,而非 mock。
implements(不使用class FakeX implements Xsuperclass)会在接口发生变化时产生 compile error,并以字段建模风险(状态而非调用顺序),同时还能作为契约文档。只将noSuchMethod用于真正的外部依赖;绝不使用mocktail/mockito(依赖 codegen,对 null-safety 没有额外帮助)。@GenerateMocks - 在 中,为传给
setUpAll/any()的每种自定义类型调用captureAny()。 缺少 fallback 时,mocktail 会在运行时而不是编译时抛错——这是最常见的 mocktail 失败原因。registerFallbackValue - 通过 无头驱动 Notifiers。 使用
ProviderContainer覆盖 provider dependencies;绝不能为了测试状态而 pump widget。对暴露的overrideWith/state 发起断言,通过AsyncValue触发 action,并在 teardown 中 dispose container。.notifier - 设置一个端到端验收门禁。 使用一个贴近现实的场景,并断言其精确的预期结果,同时验证守恒不变量(各部分之和等于总量)。这是证明各部分能够正确组合的测试。还应在基础原语内部设置运行时 assert 绊线()——在 release 构建中零成本,并能在问题发生的瞬间捕获 bug。
assert(sum(result) == total) - 通过 diff review 守住不可恢复 bug 文件的下限,而不是使用 coverage gate。 不设置自动化百分比门禁——coverage 是一份已发布的报告(由 负责)。相反,应对少数几个一旦存在测试缺口就会造成静默数据丢失的文件(migrations、money/allocate 基础原语、带 wire-format 陷阱的 parser)保持 100% 下限,并通过阅读 diff 而不是统计代码行来强制执行。目录级或全局百分比门禁只会奖励虚荣测试,并造成 exclusions 的频繁变动。首先修复
ci-pipeline-and-gates缺口:coverage-lies-upward会忽略没有任何测试导入的文件,因此该数字会高估安全性。flutter test --coverage - 绝不对无限持续的动画(splash、shimmer、spinner)调用 ——它会一直卡住,直到 10 分钟超时。使用定时
pumpAndSettle()配合pump(Duration)。fakeAsync - 保持测试套件快速,并将结构上无法测试的路径移交给人工检查。 耗时数分钟的测试套件会被跳过,而被跳过的测试套件也就不再值得信任。任何模拟器无法复现的内容(真实音频、OEM 设备差异、没有 Flutter engine 的 native surfaces)都应明确列入有名称的 manual pre-release pass,而不是伪造为通过——一个无法证明任何事情的绿色测试,比坦承存在的缺口更糟糕。
Tier the test to the code (package:test
vs flutter_test
)
package:testflutter_test根据代码选择测试层级(package:test
vs flutter_test
)
package:testflutter_testPure logic runs under with no widget binding; only UI and plugin-wrapper
tests need .
dart testflutter_testdart
// test/domain/money_test.dart — pure Dart, `dart test`, no flutter_test import.
import 'package:test/test.dart';
import 'package:app_core/app_core.dart';
void main() {
// Round-trip: encoding is lossless across the ISO-4217 exponent range.
test('minor-units round-trip for exponents 0, 2, 3', () {
for (final currency in [Currency.jpy, Currency.usd, Currency.kwd]) {
final money = Money(amountMinor: 12345, currency: currency);
expect(Money.parse(money.format(), currency), equals(money));
}
});
}纯逻辑应在 下运行,不使用 widget binding;只有 UI 和 plugin-wrapper 测试才需要 。
dart testflutter_testdart
// test/domain/money_test.dart — pure Dart, `dart test`, no flutter_test import.
import 'package:test/test.dart';
import 'package:app_core/app_core.dart';
void main() {
// Round-trip: encoding is lossless across the ISO-4217 exponent range.
test('minor-units round-trip for exponents 0, 2, 3', () {
for (final currency in [Currency.jpy, Currency.usd, Currency.kwd]) {
final money = Money(amountMinor: 12345, currency: currency);
expect(Money.parse(money.format(), currency), equals(money));
}
});
}Inject the clock; pin it in tests
注入 clock;在测试中固定时间
Production reads ; tests freeze it so time-dependent logic is
deterministic — no wall clock, no flake.
clock.now()dart
// Production: a Reminder is due when its target time has passed.
bool isDue(Reminder r) => !clock.now().isBefore(r.dueAt);
// test/domain/reminder_test.dart
test('reminder is due exactly at its due instant', () {
final due = DateTime.utc(2026, 1, 1, 9);
withClock(Clock.fixed(due), () {
expect(isDue(Reminder(dueAt: due)), isTrue);
});
});For code behind a provider, inject and freeze it in a container test
with — same , same fixed
instant, driven through Riverpod instead of the ambient zone.
clockProviderclockProvider.overrideWithValue(Clock.fixed(due))Clock生产代码读取 ;测试将其冻结,使依赖时间的逻辑保持确定性——不依赖墙上时钟,也不会出现 flaky。
clock.now()dart
// Production: a Reminder is due when its target time has passed.
bool isDue(Reminder r) => !clock.now().isBefore(r.dueAt);
// test/domain/reminder_test.dart
test('reminder is due exactly at its due instant', () {
final due = DateTime.utc(2026, 1, 1, 9);
withClock(Clock.fixed(due), () {
expect(isDue(Reminder(dueAt: due)), isTrue);
});
});对于位于 provider 之后的代码,应注入 ,并在 container test 中通过 冻结它——使用同一个 、同一个固定时刻,并通过 Riverpod 而非环境 zone 驱动。
clockProviderclockProvider.overrideWithValue(Clock.fixed(due))ClockSeeded fuzz against an independent oracle
使用带种子的 fuzz 对照独立 oracle
Twenty lines of give you what a stale property package would, with the
generated input as the built-in minimal repro. The oracle must be independent of the
function under test.
package:testdart
// test/domain/allocate_test.dart
test('allocate: parts always sum to the input (fuzz)', () {
final rng = Random(0xC0FFEE);
for (var seed = 0; seed < 500; seed++) {
final n = rng.nextInt(20) + 1;
final total = rng.nextInt(100000);
final weights = List.generate(n, (_) => rng.nextInt(500));
final parts = allocate(amountMinor: total, weights: weights);
expect(parts.fold(0, (a, b) => a + b), equals(total),
reason: 'seed=$seed total=$total weights=$weights'); // its own repro
}
});仅需二十行 就能替代陈旧的 property package,并让生成的输入天然成为最小复现。oracle 必须独立于被测函数。
package:testdart
// test/domain/allocate_test.dart
test('allocate: parts always sum to the input (fuzz)', () {
final rng = Random(0xC0FFEE);
for (var seed = 0; seed < 500; seed++) {
final n = rng.nextInt(20) + 1;
final total = rng.nextInt(100000);
final weights = List.generate(n, (_) => rng.nextInt(500));
final parts = allocate(amountMinor: total, weights: weights);
expect(parts.fold(0, (a, b) => a + b), equals(total),
reason: 'seed=$seed total=$total weights=$weights'); // its own repro
}
});Real in-memory Drift, never a mocked DAO
使用真实的内存 Drift,绝不 mock DAO
dart
// test/data/order_dao_test.dart
void main() {
late AppDatabase db;
setUp(() => db = AppDatabase(NativeDatabase.memory()));
tearDown(() => db.close()); // reactive .watch() streams must not leak timers
test('CHECK constraint rejects a negative quantity', () {
expect(
() => db.orderDao.insertItem(const OrderItem(quantity: -1)),
throwsA(isA<SqliteException>()), // proves the real constraint, not a stub
);
});
}dart
// test/data/order_dao_test.dart
void main() {
late AppDatabase db;
setUp(() => db = AppDatabase(NativeDatabase.memory()));
tearDown(() => db.close()); // reactive .watch() streams must not leak timers
test('CHECK constraint rejects a negative quantity', () {
expect(
() => db.orderDao.insertItem(const OrderItem(quantity: -1)),
throwsA(isA<SqliteException>()), // proves the real constraint, not a stub
);
});
}Headless Notifier tests with ProviderContainer
ProviderContainer使用 ProviderContainer
无头测试 Notifier
ProviderContainerdart
// test/features/cart/cart_notifier_test.dart
void main() {
test('addItem publishes the new total', () async {
final container = ProviderContainer(
overrides: [orderRepositoryProvider.overrideWith((ref) => _FakeOrderRepo())],
);
addTearDown(container.dispose);
await container.read(cartNotifierProvider.future);
await container.read(cartNotifierProvider.notifier).addItem(_anItem);
final state = container.read(cartNotifierProvider);
expect(state, isA<AsyncData<CartState>>());
expect(state.requireValue.totalMinor, 1299);
});
}ProviderContainer.test()addTearDown(container.dispose)dart
// test/features/cart/cart_notifier_test.dart
void main() {
test('addItem publishes the new total', () async {
final container = ProviderContainer(
overrides: [orderRepositoryProvider.overrideWith((ref) => _FakeOrderRepo())],
);
addTearDown(container.dispose);
await container.read(cartNotifierProvider.future);
await container.read(cartNotifierProvider.notifier).addItem(_anItem);
final state = container.read(cartNotifierProvider);
expect(state, isA<AsyncData<CartState>>());
expect(state.requireValue.totalMinor, 1299);
});
}ProviderContainer.test()addTearDown(container.dispose)Drive timers with fakeAsync
fakeAsync使用 fakeAsync
驱动定时器
fakeAsyncdart
test('debounced search fires once after the window', () {
fakeAsync((async) {
final calls = <String>[];
final debouncer = Debouncer(const Duration(milliseconds: 300));
debouncer.run(() => calls.add('a'));
async.elapse(const Duration(milliseconds: 299));
expect(calls, isEmpty);
async.elapse(const Duration(milliseconds: 1));
expect(calls, ['a']);
});
});dart
test('debounced search fires once after the window', () {
fakeAsync((async) {
final calls = <String>[];
final debouncer = Debouncer(const Duration(milliseconds: 300));
debouncer.run(() => calls.add('a'));
async.elapse(const Duration(milliseconds: 299));
expect(calls, isEmpty);
async.elapse(const Duration(milliseconds: 1));
expect(calls, ['a']);
});
});Anti-patterns
反模式
- Inverting the pyramid — driving pure logic through
f(input) -> output. Slower, flakier, hides the broken layer.pumpWidget - / ambient
DateTime.now()reachable from domain logic. Makes "identical inputs -> identical output" untestable; injectRandom()and a seed.Clock - imported into a pure package. Breaks the structural boundary and slows the fastest tier; the package should declare no Flutter SDK constraint.
flutter_test - A mocked DAO standing in for the database. Proves nothing about SQL,
constraints, indexes, or migrations. Use .
NativeDatabase.memory() - on doubles, or covering a universal claim with one lucky example instead of a fuzz loop over an independent oracle.
== - A fake that lies about its contract (a dependency that can never fail) — the test then reflects a happy-path stub, not real behaviour. Fakes must expose failure.
- Missing for a custom
registerFallbackValueargument — a runtime throw no compiler catches.any() - on an indefinite indicator — hangs on the 10-minute timeout.
pumpAndSettle() - A global/directory coverage-percentage gate — rewards vanity tests, and the raw number lies upward. Floor the unrecoverable files; publish the rest.
- A green test over a structurally-untestable path (real audio, native tile, device diversity) — worse than an admitted gap, because it stops anyone checking by hand.
- A test with no meaningful , shared mutable state across tests, or a vague name like
expect.test('works')
- 反转测试金字塔——通过 驱动纯粹的
pumpWidget逻辑。这样更慢、更易波动,还会掩盖发生故障的层级。f(input) -> output - 领域逻辑可以访问 或环境中的
DateTime.now()。 这会导致“相同输入产生相同输出”无法测试;应注入Random()和 seed。Clock - 在纯 package 中导入 。 这会破坏结构性边界并拖慢最快的测试层级;该 package 不应声明 Flutter SDK constraint。
flutter_test - 使用 mocked DAO 代替数据库。 这无法证明 SQL、constraints、indexes 或 migrations 的任何行为。应使用 。
NativeDatabase.memory() - 对 double 使用 ,或者仅用一个碰巧通过的示例覆盖某项普适性断言,而不是使用 fuzz 循环和独立 oracle。
== - 违反契约的 fake(某个永远不会失败的依赖)——此时测试反映的只是一个 happy-path stub,而非真实行为。Fake 必须能够暴露失败。
- 自定义 参数缺少
any()——会触发编译器无法捕获的运行时异常。registerFallbackValue - 对无限持续 indicator 调用 ——会一直卡住,直到 10 分钟超时。
pumpAndSettle() - 设置全局或目录级 coverage 百分比门禁——这会奖励虚荣测试,而且原始数字会向上虚报。应仅为不可恢复文件设置下限,其余 coverage 予以发布。
- 为结构上无法测试的路径提供绿色测试(真实音频、native tile、设备差异)——这比坦承缺口更糟糕,因为它会阻止任何人进行人工检查。
- 测试中没有有意义的 、测试之间共享可变状态,或者使用
expect这类含糊名称。test('works')
Definition of done
完成标准
- Each test lives at the cheapest tier that can assert it; pure logic under
, widget/integration only where the UI/plugin seam is the point.
package:test - Domain logic reads , never
clock.now(); tests pin it withDateTime.now().withClock - Every conversion has a round-trip test and boundary rounding goldens; every
universal claim is a seeded fuzz/property test against an independent oracle,
with the input echoed in .
reason: - The data layer is tested against , not a mocked DAO;
NativeDatabase.memory()in teardown.db.close() - Owned collaborators are bare-fakes;
implementsis reserved for external deps withmocktailfor every customregisterFallbackValuetype.any() - Notifiers are tested through overrides, disposed in teardown; no widget pumped to test state.
ProviderContainer - One acceptance gate asserts a realistic scenario exactly and the conservation
invariant; the primitive carries a runtime tripwire.
assert - Coverage floors sit on the unrecoverable-bug files; the coverage-lies-upward gap is fixed (untested files included); generated files stripped.
- No on an indefinite animation; timers driven by
pumpAndSettle().fakeAsync - The suite stays within its time budget; every structurally-untestable path is named in the manual pre-release pass, not faked green.
- 每个测试都位于能够断言其行为且成本最低的层级;纯逻辑使用 ,只有在 UI/plugin seam 本身是测试目标时才使用 widget/integration。
package:test - 领域逻辑读取 ,绝不读取
clock.now();测试使用DateTime.now()固定时间。withClock - 每种转换都有 round-trip 测试和边界 rounding golden;每项普适性断言都有带种子的 fuzz/property test,并对照独立 oracle,同时在 中回显输入。
reason: - 数据层针对 测试,而不是 mocked DAO;在 teardown 中调用
NativeDatabase.memory()。db.close() - 自己拥有的协作者使用 bare-fake;
implements仅用于外部 deps,并为每种自定义mocktail类型设置any()。registerFallbackValue - Notifiers 通过 overrides 测试,并在 teardown 中 dispose;不通过 pump widget 测试状态。
ProviderContainer - 一个 acceptance gate 精确断言现实场景和守恒不变量;基础原语包含运行时 绊线。
assert - Coverage 下限设置在不可恢复 bug 的文件上;修复 缺口(纳入未测试文件);排除生成文件。
coverage-lies-upward - 不对无限持续动画调用 ;使用
pumpAndSettle()驱动定时器。fakeAsync - 测试套件保持在时间预算内;每条结构上无法测试的路径都明确列入 manual pre-release pass,而不是伪造为通过。
Related skills
相关 skills
- See for golden lanes, the overflow/RTL matrix, and honest a11y limits.
widget-golden-and-a11y-testing - See for the
value-objects-money-and-unitsprimitive andallocate()injection this skill tests.Clock - See for the DAO/value-object seam the in-memory tests exercise.
persistence-drift - See for the forward-only migration ritual whose from→to paths get the 100% floor.
run-migration - See for the sealed
error-handling-typed-results/Resultthe exhaustive-switch tests assert on.Failure - See for the dropped-Future holes the silence-review checklist targets.
async-safety - See for the injectable ports the fakes stand in for.
service-boundary-and-native - See for how these lanes and greps are wired as gates.
ci-pipeline-and-gates - See for pinning a seeded generator with a frozen fingerprint table when output must reproduce across devices and releases.
seeded-determinism-and-golden-vectors
- 参见 ,了解 golden lanes、overflow/RTL matrix,以及真实的 a11y 限制。
widget-golden-and-a11y-testing - 参见 ,了解本 skill 所测试的
value-objects-money-and-units基础原语和allocate()注入。Clock - 参见 ,了解内存测试所覆盖的 DAO/value-object seam。
persistence-drift - 参见 ,了解 forward-only migration 流程及其获得 100% 下限的 from→to paths。
run-migration - 参见 ,了解 exhaustive-switch 测试所断言的 sealed
error-handling-typed-results/Result。Failure - 参见 ,了解 silence-review checklist 所针对的 dropped-Future 漏洞。
async-safety - 参见 ,了解 fake 所替代的 injectable ports。
service-boundary-and-native - 参见 ,了解这些 lanes 和 greps 如何被接入 gates。
ci-pipeline-and-gates - 参见 ,了解如何在输出必须跨设备和 release 可复现时,使用冻结的 fingerprint table 固定带种子的 generator。
seeded-determinism-and-golden-vectors
References
参考资料
- Flutter testing overview — https://docs.flutter.dev/testing/overview
- Riverpod testing — https://riverpod.dev/docs/essentials/testing
- Drift testing (NativeDatabase.memory) — https://drift.simonbinder.eu/docs/testing/
- — https://pub.dev/packages/clock
package:clock - — https://pub.dev/packages/fake_async
package:fake_async - — https://pub.dev/packages/mocktail
package:mocktail
- Flutter 测试概览 — https://docs.flutter.dev/testing/overview
- Riverpod 测试 — https://riverpod.dev/docs/essentials/testing
- Drift 测试(NativeDatabase.memory)— https://drift.simonbinder.eu/docs/testing/
- — https://pub.dev/packages/clock
package:clock - — https://pub.dev/packages/fake_async
package:fake_async - — https://pub.dev/packages/mocktail
package:mocktail