Approval Testing and why it’s important | Dart 🎯
Tags: dart, approvaltests, testing, approvals
Published: 2024-05-22 13:17:33


[Approval Tests](https://approvaltests.com/) provide an alternative approach to traditional assertions in software testing. They are particularly useful when dealing with complex objects, such as long strings, collections, or objects with many properties. By capturing a snapshot of the output and comparing it with an approved version, Approval Tests simplify the process of verifying that your code behaves as expected. This article will introduce the [Dart implementation](https://pub.dev/packages/approval_tests) of Approval Tests and demonstrate its use through an example.

## What are Approval Tests?
Approval Tests offer a different way to perform assertions in tests. Traditional assertions compare a result directly to an expected value, like this:
`expect(a, equals(b));`
In contrast, Approval Tests use a verification approach:
`Approvals.verify(a);`

Here’s how it works:

1. If the results match the approved file perfectly, the test passes.
2. If there’s a difference, a reporter tool highlights the mismatch, and the test fails, providing a visual and textual representation of the changes.



![Running Approvals](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3a5x3myuxiu1dld1v4d.png)

If you are fixing a bug (or adding a feature), this will change the expected behavior. Therefore when you run the test, it will fail.

![Add Behavior to Existing Approval](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lv7b9f059wjtgnwy526.png)

## Why Use Approval Tests?
Approval Tests streamline the feedback loop, saving developers time by only highlighting what has changed rather than requiring them to parse through the entire output. This approach is especially beneficial for complex outputs where traditional assertions can be cumbersome.

## Dart Implementation of Approval Tests
To use Approval Tests in your Dart project, add the following to your `pubspec.yaml` file:

``` yaml
dependencies:
  approval_tests: ^1.0.0
```

## Getting Started
Download and open the starter project: [Approvaltests.Dart.StarterProject](https://github.com/approvals/Approvaltests.Dart.StarterProject)

This project includes:

- A `.gitignore` file to exclude approval artifacts.
- A linter with all rules in place.
- A GitHub action to run tests, with the test status visible on the `README.md` badge.

## To use Approval Tests:
1. Set up a test: Import the Approval Tests library into your code.
2. Optionally, set up a reporter: Reporters highlight differences between approved and received files when a test fails. The default reporter is the `CommandLineReporter`. You can also use the `DiffReporter` to compare files in your IDE. The package currently supports `VS Code` and `Android Studio`. If by some errors or other reason you want to make your own, there is a customDiffInfo option.
3. Manage the “approved” file: When the test is run for the first time, an approved file is created automatically. This file represents the expected outcome. Once the test results are satisfactory, update the approved file to reflect these changes.

## Approving Results
Approving results involves saving the `.approved.txt` file with your desired results. Common approaches include:

- Using a diff tool to move text from left to right and save the result.
- Using the `approveResult` property in `Options` for automatic approval after running the test.
- Renaming the `.received` file to `.approved`.

## Reporters
Reporters launch diff tools when things do not match, making it easy to see changes. Available reporters include:

- `CommandLineReporter`: Outputs the diff in the terminal.

![CommandLineReporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jlr6lm48p9te7izu30g.png)

- `DiffReporter`: Opens the Diff Tool in your IDE.

![VS Code DiffReporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cfy70kn9s2ytkygl18el.png)

## Example: Gilded Rose Kata
The following example demonstrates how to use Approval Tests to refactor the `GildedRose` non-refactored code from the [Gilded Rose Kata](https://github.com/emilybache/GildedRose-Refactoring-Kata):

```dart
final class GildedRose {
  final List<Item> items;

  GildedRose({required this.items});

  void updateQuality() {
    for (int i = 0; i < items.length; i++) {
      if (items[i].name != "Aged Brie" &&
          items[i].name != "Backstage passes to a TAFKAL80ETC concert") {
        if (items[i].quality > 0) {
          if (items[i].name != "Sulfuras, Hand of Ragnaros") {
            items[i].quality = items[i].quality - 1;
          }
        }
      } else {
        if (items[i].quality < 50) {
          items[i].quality = items[i].quality + 1;
          if (items[i].name == "Backstage passes to a TAFKAL80ETC concert") {
            if (items[i].sellIn < 11) {
              if (items[i].quality < 50) {
                items[i].quality = items[i].quality + 1;
              }
            }
            if (items[i].sellIn < 6) {
              if (items[i].quality < 50) {
                items[i].quality = items[i].quality + 1;
              }
            }
          }
        }
      }
      if (items[i].name != "Sulfuras, Hand of Ragnaros") {
        items[i].sellIn = items[i].sellIn - 1;
      }
      if (items[i].sellIn < 0) {
        if (items[i].name != "Aged Brie") {
          if (items[i].name != "Backstage passes to a TAFKAL80ETC concert") {
            if (items[i].quality > 0) {
              if (items[i].name != "Sulfuras, Hand of Ragnaros") {
                items[i].quality = items[i].quality - 1;
              }
            }
          } else {
            items[i].quality = items[i].quality - items[i].quality;
          }
        } else {
          if (items[i].quality < 50) {
            items[i].quality = items[i].quality + 1;
          }
        }
      }
    }
  }
}

final class Item {
  final String name;
  int sellIn;
  int quality;

  Item(this.name, {required this.sellIn, required this.quality});

  @override
  String toString() => 'Item{name: $name, sellIn: $sellIn, quality: $quality}';
}

```
Test code:

```dart
import 'package:approval_tests/approval_tests.dart';
import 'package:starter_project/starter_project.dart';
import 'package:test/test.dart';

void main() {
  // Define all test cases
  const allTestCases = [
    [
      "foo",
      "Aged Brie",
      "Backstage passes to a TAFKAL80ETC concert",
      "Sulfuras, Hand of Ragnaros",
    ],
    [-1, 0, 5, 6, 10, 11],
    [-1, 0, 1, 49, 50],
  ];

  group('Approval Tests for Gilded Rose', () {
    test('verify all combinations', () {
      Approvals.verifyAllCombinations(
        allTestCases,
        // options: const Options(
        //   reporter: DiffReporter(),
        // ),
        processor: processItemCombination,
      );
    });
  });
}

// Function to process each combination and generate output for verification
String processItemCombination(Iterable<List<dynamic>> combinations) {
  final receivedBuffer = StringBuffer();

  for (final combination in combinations) {
    // Extract data from the current combination
    final String itemName = combination[0] as String;
    final int sellIn = combination[1] as int;
    final int quality = combination[2] as int;

    // Create an Item object representing the current combination
    final Item testItem = Item(itemName, sellIn: sellIn, quality: quality);

    // Passing testItem to the application
    final GildedRose app = GildedRose(items: [testItem]);

    // Updating quality of testItem
    app.updateQuality();

    // Adding the updated item to expectedItems
    receivedBuffer.writeln(testItem.toString());
  }

  // Return a string representation of the updated item
  return receivedBuffer.toString();
}
```

And at the output we get **120 different combinations** based on which we can start refactoring our code. Of course, you can divide the combination generation into parts and remove unnecessary combinations, but this is just an example.


![120 combinations for Gilded Rose Kata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x8pefxgovqnr3dqej85l.png)

## Conclusion

Approval Tests for Dart offer a powerful alternative to traditional assertions, particularly for complex objects and outputs. By simplifying the verification process, they help developers focus on what has changed, reducing the time and effort needed to understand the impact of their code changes. For more information, examples, and how to contribute, visit the [Approval Tests Dart repository](https://github.com/approvals/ApprovalTests.Dart).

Please note, this is my first article and my initial public library. I’m actively communicating with Approval Tests creator Llewelyn Falco about improvements to the library. Your suggestions, issues, and pull requests are highly encouraged and welcomed. Let’s make this library even better together! 🙌

Show some 💙 and star the [repo](https://github.com/approvals/ApprovalTests.Dart) to support the project! 🫰

For any questions, feel free to reach out via [Telegram](https://t.me/yelmuratoff) or email at [yelaman.yelmuratov@gmail.com](mailto:yelamanyelmuratov@gmail.com).