Approval Tests are an alternative to assertions. Youβll find them useful for testing objects with complex values (such as long strings), lots of properties, or collections of objects.
Approval tests simplify this by taking a snapshot of the results, and confirming that they have not changed.
In normal unit testing, you say expect(person.getAge(), 5). Approvals allow you to do this when the thing that you want to assert is no longer a primitive but a complex object. Approvals.verify() accepts text; for a model with a toJson() method, use Approvals.verifyAsJson(person.toJson()).
I am writing an implementation of Approval Tests in Dart. If anyone wants to help, please text me. π
ApprovalTests is designed for two level: Dart and Flutter.
| Package | Version | Description |
|---|---|---|
| approval_tests | Dart package for approval testing of unit tests (main) |
|
| approval_tests_flutter | Flutter package for approval testing of widget, integration tests |
- The first run of the test automatically creates an
approvedfile if there is no such file. - If the changed results match the
approvedfile perfectly, the test passes. - If there's a difference, a
reportertool will highlight the mismatch and the test fails. - If the test is passed, the
receivedfile is deleted automatically. You can change this by changing thedeleteReceivedFilevalue inoptions. If the test fails, the received file remains for analysis.
The default policy preserves the 1.x first-run workflow: verification creates the missing approved file and passes. Use strict mode when verification must never create or update approved artifacts, such as in CI:
Approvals.verify(
response,
options: const Options(
missingApprovedPolicy: MissingApprovedPolicy.writeReceivedAndFail,
),
);Strict mode writes a reviewable received file and throws a
DoesntMatchException whose kind is
ApprovalMismatchKind.missingApproved. Generate or approve the expected file
locally after reviewing it; dart run approval_tests:review is the preferred
review workflow. The approveResult option is a deliberate local migration
tool and is ignored by strict mode so verification cannot mutate an approved
artifact.
Test names, descriptions, custom file names, and final artifact names are
validated as filename segments. / and \ are rejected with
InvalidApprovalNameException instead of being interpreted as directories.
Control characters, Windows-invalid characters, trailing dots or spaces, and
reserved names such as CON are normalized consistently on every platform.
Existing valid names remain unchanged.
Generated filename segments are limited to 255 UTF-8 bytes. Longer names keep
a readable prefix and receive a stable 16-character hash while preserving the
artifact extension. If two verifications in one loaded test suite target the
same normalized path, the second fails before writing with
ApprovalPathCollisionException; the exception exposes the path and both
verification identities.
Approved and received paths are validated and claimed together before any
artifact is written. If either path is invalid, neither path remains claimed.
When one test performs several logical approvals, give each verification a
unique description or use IndexedNamer so their artifact paths do not
collide.
ApprovalTextWriter writes to a same-directory temporary file, flushes it,
and atomically replaces the destination. Concurrent readers therefore observe
either the previous complete artifact or the new complete artifact, never a
partially written file. On Windows, transient access, sharing, and lock
violations caused by concurrent readers are retried with a bounded attempt
count. Temporary files are cleaned up if replacement fails.
Add the following to your pubspec.yaml file:
dependencies:
approval_tests: ^1.6.1These docs target the 1.6.1 release. Dart 3.6 or newer has been
required since 1.5.0 because the internal console logger uses
ispectify 6.1.2.
The best way to get started is to download and open the starter project:
This is a standard project that can be imported into any editor or IDE and also includes CI with GitHub Actions.
It comes ready with:
- A suitable
.gitignorethat ignores received artifacts while keeping approved artifacts under source control - A ready linter with all rules in place
- A GitHub action to run tests and you can always check the status of the tests on the badge in the
README.mdfile.
In order to use Approval Tests, the user needs to:
-
Set up a test: This involves importing the Approval Tests library into your own code.
-
Optionally, set up a reporter: Reporters are tools that highlight differences between approved and received files when a test fails. Although not necessary, they make it significantly easier to see what changes have caused a test to fail. The default reporter is the
CommandLineReporter. You can also use theDiffReporterto compare the files in your IDE, and theGitReporterto see the differences in theGit GUI. -
Manage the
approvedfile: When the test is run for the first time, an approved file is created automatically. This file will represent the expected outcome. Once the test results in a favorable outcome, the approved file should be updated to reflect these changes. A little bit below I wrote how to do it.
This setup is useful because it shortens feedback loops, saving developers time by only highlighting what has been altered rather than requiring them to parse through their entire output to see what effect their changes had.
Approving results just means saving the .approved.txt file with your desired results.
Weβll provide more explanation in due course, but, briefly, here are the most common approaches to do this.
Most diff tools have the ability to move text from left to right, and save the result.
How to use diff tools is just below, there is a Comparator class for that.
You can run the command in a terminal to review your files:
dart run approval_tests:reviewAfter running the command, the files will be analyzed and you will be asked to choose one of the options:
y- Approve the received file.n- Reject the received file.view - View the differences between the received and approved files. After selectingvyou will be asked which IDE you want to use to view the differences.
With no arguments, the command discovers supported .received.txt files,
sorts them by normalized relative path, and reviews them one at a time so only
one terminal prompt is active. --list prints that same deterministic order;
you can then pass an index or a path from the list. Paths must identify an
existing .received.txt file. Diff tools are awaited, so launch and execution
failures are reported instead of being silently ignored.
If you want the result to be automatically saved after running the test, you need to use the approveResult property in Options:
approveResultis intended for a deliberate, local migration or initial snapshot-generation step. Remove it after reviewing the generated file, and never enable it in normal CI: CI should verify approved artifacts, not mutate them.
void main() {
test('test JSON object', () {
final complexObject = {
'name': 'JsonTest',
'features': ['Testing', 'JSON'],
'version': 0.1,
};
Approvals.verifyAsJson(
complexObject,
options: const Options(
approveResult: true,
),
);
});
}this will result in the following file
example_test.test_JSON_object.approved.txt
# This file was generated by approval_tests. Please do not edit.
{
"name": "JsonTest",
"features": [
"Testing",
"JSON"
],
"version": 0.1
}You can just rename the .received file to .approved.
Reporters are the part of Approval Tests that launch diff tools when things do not match. They are the part of the system that makes it easy to see what has changed.
There are several reporters available in the package:
CommandLineReporter- This is the default reporter, which will output the diff in the terminal.GitReporter- This reporter will open the diff in the Git GUI usinggit diff --no-index. Each argument is provided separately to Git and any exit code greater than1is surfaced as a failure, so unexpected tool errors are easier to spot. Provide a customDiffInfoif you need extra arguments (for example,DiffInfo(command: 'git', arg: 'diff --no-index --word-diff')).DiffReporter- This reporter will open the Diff Tool in your IDE. Arguments are tokenized automatically, so values such as-d --waitcan be supplied in theDiffInfo.argstring without additional escaping.- For Diff Reporter I using the default paths to the IDE, if something didn't work then you in the console see the expected correct path to the IDE and specify customDiffInfo. You can also contact me for help.
To use DiffReporter you just need to add it to options:
options: const Options(
reporter: const DiffReporter(),
),Approval Tests emits compact, typed console diagnostics through ispectify.
Exceptions retain their original stack traces, while in-memory log history is
disabled so test runs do not accumulate diagnostic events. This internal
logging change does not alter approved or received file contents and requires
no application-level logger setup.
Use CompositeScrubber when a snapshot contains more than one kind of
volatile value. Scrubbers run in the order they are supplied:
Approvals.verify(
response,
options: Options(
scrubber: CompositeScrubber([
const ScrubDates(),
const ScrubWithRegEx(),
]),
),
);Use aliases when repeated volatile values carry meaning. The same source value
receives the same alias within one scrub() call, while distinct values are
numbered by first appearance. Alias state resets for every call.
Approvals.verify(
response,
options: Options(
scrubber: CompositeScrubber([
const ScrubUuids(),
const ScrubWithAliases(
pattern: r'user-\d+',
alias: 'user',
),
]),
),
);ScrubUuids treats case variants of a canonical UUID as the same value.
ScrubWithAliases is case-sensitive by default; set caseSensitive: false
when case should not affect alias identity.
I have provided a couple of small examples here to show you how to use the package.
There are more examples in the example folder for you to explore. I will add more examples in the future.
Inside, in the gilded_rose folder, there is an example of using ApprovalTests to test the legacy code of Gilded Rose kata.
You can study it to understand how to use the package to test complex code.
And the verify_methods folder has small examples of using different ApprovalTests methods for different cases.
NetworkRequestQuery now supports stubbed responses so approval files can be generated deterministically without a live HTTP request. You can still call real services, but the default constructor parameters return a cached payload for the provided URI:
final query = NetworkRequestQuery(
Uri.parse('https://jsonplaceholder.typicode.com/todos/1'),
stubbedResponses: {
Uri.parse('https://jsonplaceholder.typicode.com/todos/1'): {
'id': 1,
'status': 'OK',
},
},
);
await Approvals.verifyQuery(query);example/verify_methods/verify_query/verify_network_query_test.dart demonstrates how to plug the query into an approval test.
With verifyAsJson, if you pass data models as JsonItem, with nested other models as AnotherItem and SubItem, then you need to add an toJson method to each model for the serialization to succeed.
void main() {
const jsonItem = JsonItem(
id: 1,
name: "JsonItem",
anotherItem: AnotherItem(id: 1, name: "AnotherItem"),
subItem: SubItem(
id: 1,
name: "SubItem",
anotherItems: [
AnotherItem(id: 1, name: "AnotherItem 1"),
AnotherItem(id: 2, name: "AnotherItem 2"),
],
),
);
test('verify model', () {
Approvals.verifyAsJson(
jsonItem,
options: const Options(
approveResult:
true, // Approve the result automatically. You can remove this property after the approved file is created.
),
);
});
}this will result in the following file
verify_as_json_test.verify_model.approved.txt
# This file was generated by approval_tests. Please do not edit.
{
"jsonItem": {
"id": 1,
"name": "JsonItem",
"subItem": {
"id": 1,
"name": "SubItem",
"anotherItems": [
{
"id": 1,
"name": "AnotherItem 1"
},
{
"id": 2,
"name": "AnotherItem 2"
}
]
},
"anotherItem": {
"id": 1,
"name": "AnotherItem"
}
}
}Commit every *.approved.* artifact: it is the reviewed test expectation.
Ignore every *.received.* artifact because it is regenerated when a
verification fails. For Git, use:
*.received.*Do not add *.approved.* to .gitignore.
Ask me on Telegram: @yelmuratoff.
Email: yelamanyelmuratov@gmail.com
- Getting Started with ApprovalTests.Swift
- How to Verify Objects (and Simplify TDD)
- Verify Arrays and See Simple, Clear Diffs
- Write Parameterized Tests by Transforming Sequences
- Wrangle Legacy Code with Combination Approvals
You can also watch a series of short videos about using ApprovalTests in .Net on YouTube.
Prefer learning by listening? Then you might enjoy the following podcasts:
The 1.6.1 release has 100% line coverage for executable code under lib
(742/742 lines). The full suite and a randomized-order run each pass all 170
test executions.
To reproduce the line-coverage report locally:
dart pub global activate coverage
dart test --coverage=coverage
dart pub global run coverage:format_coverage \
--packages=.dart_tool/package_config.json \
--report-on=lib \
--lcov \
-o coverage/lcov.info \
-i coverageShow some π and star the repo to support the project! π
The project is in the process of development and we invite you to contribute through pull requests and issue submissions. π
We appreciate your support. π«°



