flutter_jest_style
A Jest-like testing experience for Flutter & Dart.
Snapshot testing Β· Mock helpers Β· Watch-mode CLI Β· Visual regression β all in one import.
π€ Why This Package?
If you're coming from JavaScript/TypeScript and miss Jest's ergonomics, this package bridges the gap:
| Jest Feature | Flutter Equivalent | This Package |
|---|---|---|
toMatchSnapshot() |
No built-in equivalent | β
toMatchSnapshot('name') β auto-creates & compares .snap files |
jest.fn() |
mockito + build_runner |
β
fn<T>() β zero-codegen mock functions via mocktail |
jest --watch |
flutter test (manual re-run) |
β
fjest --watch β auto-reruns on file save |
jest --updateSnapshot |
N/A | β
fjest --snapshot-update |
jest --coverage |
flutter test --coverage |
β
fjest --coverage (same, wrapped) |
| Visual regression | matchesGoldenFile() |
β
goldenPath('name') β scaffolds directories automatically |
| Single import | Multiple imports needed | β
One import gives you everything |
π¦ Installation
Add to your pubspec.yaml:
dependencies:
flutter_jest_style: ^1.0.0
Or install via the command line:
dart pub add flutter_jest_style
π Quick Start
import 'package:flutter_jest_style/flutter_jest_style.dart';
void main() {
group('Calculator', () {
test('adds two numbers', () {
expect(1 + 2, equals(3));
});
test('result matches snapshot', () {
final result = {'operation': 'add', 'a': 1, 'b': 2, 'result': 3};
expect(result, toMatchSnapshot('calculator_add'));
});
});
}
One import. Familiar syntax. Snapshots just work.
β¨ Features
πΈ Snapshot Testing
Replicate Jest's toMatchSnapshot() β on the first run the snapshot file is created; subsequent runs compare against it.
import 'package:flutter_jest_style/flutter_jest_style.dart';
void main() {
test('user serialises correctly', () {
final user = {'name': 'Alice', 'age': 30, 'roles': ['admin', 'editor']};
expect(user, toMatchSnapshot('user_profile'));
});
}
File structure produced:
test/
βββ __snapshots__/
β βββ user_profile.snap β auto-generated JSON
βββ my_test.dart
Updating snapshots when your data intentionally changes:
fjest --snapshot-update
Or programmatically in a setUp:
import 'package:flutter_jest_style/flutter_jest_style.dart';
void main() {
setUp(() => updateSnapshots = true);
test('regenerate all snapshots', () {
expect({'key': 'new_value'}, toMatchSnapshot('my_snapshot'));
});
}
π Mocking (jest.fn() Style)
Zero-codegen mock functions powered by mocktail:
import 'package:flutter_jest_style/flutter_jest_style.dart';
void main() {
test('fn<T>() with stubbed return', () {
final myFn = fn<int>();
mockReturnValue(myFn, 42);
expect(myFn(), equals(42));
verify(() => myFn()).called(1);
});
test('fn<T>() with implementation', () {
final counter = fn<int>();
var n = 0;
mockImplementation(counter, () => ++n);
expect(counter(), equals(1));
expect(counter(), equals(2));
});
test('fn1 β mock with one argument', () {
final greet = fn1<String, String>();
when(() => greet(any())).thenReturn('Hello!');
expect(greet('Alice'), equals('Hello!'));
verify(() => greet('Alice')).called(1);
});
test('reset and clear', () {
final myFn = fn<int>();
mockReturnValue(myFn, 10);
myFn();
mockClear(myFn); // clears calls, keeps stubs
mockReset(myFn); // clears everything
});
}
πΌοΈ Visual / Golden Testing
Golden (pixel-perfect) regression testing with automatic directory scaffolding:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_jest_style/flutter_jest_style.dart';
void main() {
testWidgets('MyButton matches golden', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: Scaffold(body: MyButton())),
);
await expectLater(
find.byType(MyButton),
matchesGoldenFile(goldenPath('my_button')),
);
});
}
File structure:
test/
βββ goldens/
β βββ my_button.png β auto-generated golden image
βββ widget_test.dart
Generate/update golden files:
flutter test --update-goldens
β‘ Watch Mode CLI
The fjest CLI wraps flutter test with a Jest-inspired workflow:
# Run all tests once
fjest
# Watch mode β auto-reruns on file changes in lib/ and test/
fjest --watch
# Collect code coverage
fjest --coverage
# Regenerate all snapshots
fjest --snapshot-update
# Combine flags
fjest --watch --coverage
# Forward extra flags to flutter test
fjest -- --name "my specific test" --reporter expanded
π Coverage Reporting
# Generate coverage
fjest --coverage
# View HTML report (requires lcov)
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
π§ Advanced Usage
Updating Snapshots
When your data model changes intentionally:
# Update all snapshots via CLI
fjest --snapshot-update
# Or set the flag programmatically
updateSnapshots = true;
Ignoring Fields in Snapshots
Sanitise volatile fields (timestamps, IDs) before snapshotting:
test('user snapshot ignoring timestamp', () {
final user = getUser();
final sanitised = Map<String, dynamic>.from(user)
..remove('createdAt')
..remove('id');
expect(sanitised, toMatchSnapshot('user_no_volatile'));
});
Async Testing
test('fetches data asynchronously', () async {
final fetchData = fn<Future<String>>();
when(() => fetchData()).thenAnswer((_) async => 'result');
final result = await fetchData();
expect(result, equals('result'));
verify(() => fetchData()).called(1);
});
Running in CI/CD (GitHub Actions)
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: stable
- run: dart pub get
- run: dart analyze
- run: dart format --output=none --set-exit-if-changed .
- run: dart test
env:
FLUTTER_JEST_ROOT: ${{ github.workspace }}
Tip: Setting
FLUTTER_JEST_ROOTensures snapshot paths resolve correctly regardless of the runner's working directory.
π API Reference
Snapshot Testing
| API | Description |
|---|---|
toMatchSnapshot(String name) |
Matcher β compares value against test/__snapshots__/<name>.snap. Creates on first run. |
updateSnapshots (global) |
Set to true to overwrite all snapshots instead of comparing. |
Mock Utilities
| API | Description |
|---|---|
fn<T>({T? fallback}) |
Creates a zero-arg mock function (like jest.fn()). |
fn1<R, A>() |
Creates a one-arg mock function. |
fn2<R, A, B>() |
Creates a two-arg mock function. |
mockReturnValue<T>(mock, value) |
Stubs a return value (like jest.fn().mockReturnValue()). |
mockImplementation<T>(mock, impl) |
Stubs with a callback (like jest.fn().mockImplementation()). |
mockReset(mock) |
Clears all stubs and recorded calls. |
mockClear(mock) |
Clears recorded calls but keeps stubs. |
Golden Utilities
| API | Description |
|---|---|
goldenPath(String name) |
Returns test/goldens/<name>.png, creating the directory if needed. |
CLI (fjest)
| Flag | Description |
|---|---|
--watch, -w |
Watch mode β rerun tests on file changes. |
--coverage, -c |
Collect code coverage. |
--snapshot-update, -u |
Regenerate all snapshot files. |
--help, -h |
Print usage information. |
Re-exports
Everything from package:test and package:mocktail is re-exported, so you get test(), group(), expect(), setUp(), tearDown(), when(), verify(), any(), etc. with a single import.
π Migration Guide β Coming from Jest?
Basic Test Structure
| Jest (JavaScript) | flutter_jest_style (Dart) |
|---|---|
|
|
Snapshots
| Jest | flutter_jest_style |
|---|---|
|
|
Mocking
| Jest | flutter_jest_style |
|---|---|
|
|
Lifecycle Hooks
| Jest | flutter_jest_style |
|---|---|
|
|
Full Mapping Table
| Jest | flutter_jest_style | Notes |
|---|---|---|
describe() |
group() |
|
test() / it() |
test() |
|
expect(x).toBe(y) |
expect(x, equals(y)) |
|
expect(x).toBeTruthy() |
expect(x, isTrue) |
|
expect(x).toBeNull() |
expect(x, isNull) |
|
expect(x).toContain(y) |
expect(x, contains(y)) |
|
expect(fn).toThrow() |
expect(() => fn(), throwsA(anything)) |
|
expect(x).toMatchSnapshot() |
expect(x, toMatchSnapshot('name')) |
Named snapshots |
jest.fn() |
fn<T>() |
|
fn.mockReturnValue(v) |
mockReturnValue(mock, v) |
|
fn.mockImplementation(cb) |
mockImplementation(mock, cb) |
|
fn.mockReset() |
mockReset(mock) |
|
fn.mockClear() |
mockClear(mock) |
|
jest --watch |
fjest --watch |
|
jest --updateSnapshot |
fjest --snapshot-update |
|
jest --coverage |
fjest --coverage |
β Troubleshooting
test/__snapshots__ directory not found in CI
Symptom: Snapshot path resolves to the wrong location in Docker / GitHub Actions.
Fix: Set the FLUTTER_JEST_ROOT environment variable to your project root:
# GitHub Actions
- run: dart test
env:
FLUTTER_JEST_ROOT: ${{ github.workspace }}
# Docker / generic CI
export FLUTTER_JEST_ROOT=/app
dart test
Snapshot mismatch after upgrading a dependency
Symptom: Tests fail with "Snapshot mismatch" after a pub upgrade.
Fix: Review the diff in the error output. If the change is expected, update snapshots:
fjest --snapshot-update
Then commit the updated .snap files.
MissingStubError when calling a mock
Symptom: fn<T>() throws MissingStubError when called without a stub.
Fix: Either stub first or provide a fallback:
// Option A: stub explicitly
final myFn = fn<int>();
mockReturnValue(myFn, 0);
// Option B: use fallback parameter
final myFn = fn<int>(fallback: 0);
Golden file mismatch on different platforms
Symptom: Golden tests pass on macOS but fail on Linux CI.
Fix: Generate golden files on the same platform as CI (typically Linux). Use:
# On CI
flutter test --update-goldens
Then commit the goldens from that environment.
π€ Contributing
Contributions are welcome! Here's how:
- Report bugs β Open an issue with steps to reproduce.
- Suggest features β Describe the use case in an issue first.
- Submit PRs:
git clone https://github.com/AshwiniJoshi/flutter_jest_style.git cd flutter_jest_style dart pub get dart test # ensure tests pass dart analyze # ensure zero issues dart format . # ensure code is formatted - Write tests for any new functionality.
- Open a PR against
main.
Please follow the Dart style guide.
π License & Credits
MIT License β see LICENSE for details.
Built on the shoulders of:
testβ Dart's core test frameworkmocktailβ zero-codegen mockinggolden_toolkitβ advanced golden testingwatcherβ file-system change detectionmatcherβ custom matcher foundations
Libraries
- flutter_jest_style
- A Jest-like testing experience for Flutter & Dart.