Skip to content

approvals/ApprovalTests.Dart

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

328 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Approval Tests implementation in Dart πŸš€


codecov Pub License: APACHE Stars

Pub likes Pub points

Build and test badge Deploy and Create Release mdsnippets

πŸ“– About

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. πŸ™

Packages

ApprovalTests is designed for two level: Dart and Flutter.

Package Version Description
approval_tests Pub Dart package for approval testing of unit tests (main)
approval_tests_flutter Pub Flutter package for approval testing of widget, integration tests

πŸ“‹ How it works

  • The first run of the test automatically creates an approved file if there is no such file.
  • If the changed results match the approved file perfectly, the test passes.
  • If there's a difference, a reporter tool will highlight the mismatch and the test fails.
  • If the test is passed, the received file is deleted automatically. You can change this by changing the deleteReceivedFile value in options. If the test fails, the received file remains for analysis.

Missing approved files

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.

Safe artifact names

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.

Atomic text artifacts

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.

πŸ“¦ Installation

Add the following to your pubspec.yaml file:

dependencies:
  approval_tests: ^1.6.1

These 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.

πŸ‘€ Getting Started

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 .gitignore that 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.md file.

πŸ“š How to use

In order to use Approval Tests, the user needs to:

  1. Set up a test: This involves importing the Approval Tests library into your own code.

  2. 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 the DiffReporter to compare the files in your IDE, and the GitReporter to see the differences in the Git GUI.

  3. Manage the approved file: 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

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.

β€’ Via Diff Tool

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.

β€’ Via CLI command

You can run the command in a terminal to review your files:

dart run approval_tests:review

After 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 selecting v you 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.

β€’ Via approveResult property

If you want the result to be automatically saved after running the test, you need to use the approveResult property in Options:

approveResult is 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,
      ),
    );
  });
}

snippet source | anchor

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
}

snippet source | anchor

β€’ Via file rename

You can just rename the .received file to .approved.

Reporters

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 using git diff --no-index. Each argument is provided separately to Git and any exit code greater than 1 is surfaced as a failure, so unexpected tool errors are easier to spot. Provide a custom DiffInfo if 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 --wait can be supplied in the DiffInfo.arg string 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.

CommandLineComparator img

To use DiffReporter you just need to add it to options:

 options: const Options(
   reporter: const DiffReporter(),
 ),

Console diagnostics

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.

Visual Studio code img Android Studio img

Scrubber pipelines

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.

πŸ“ Examples

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.

Network query example

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.

JSON example

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.
      ),
    );
  });
}

snippet source | anchor

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"
    }
  }
}

snippet source | anchor

Passed test example

❓ Which File Artifacts to Exclude from Source Control

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.

βœ‰οΈ For More Information

Questions?

Ask me on Telegram: @yelmuratoff.
Email: yelamanyelmuratov@gmail.com

Video Tutorials

You can also watch a series of short videos about using ApprovalTests in .Net on YouTube.

Podcasts

Prefer learning by listening? Then you might enjoy the following podcasts:

Coverage

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 coverage

🀝 Contributing

Show 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. 🫰



Thanks to all contributors of this package


About

Approval Tests implementation in Dart

Topics

Resources

License

Code of conduct

Contributing

Stars

14 stars

Watchers

2 watching

Forks

Packages

 
 
 

Contributors