Approval Testing and why it’s important | Dart 🎯

yelmuratoff

Yelaman Yelmuratov

Posted on May 22, 2024

Approval Testing and why it’s important | Dart 🎯

Approval Tests 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 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

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

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:

dependencies:
  approval_tests: ^1.0.0
Enter fullscreen mode Exit fullscreen mode

Getting Started

Download and open the starter project: 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

  • DiffReporter: Opens the Diff Tool in your IDE.

VS Code DiffReporter

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:

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}';
}

Enter fullscreen mode Exit fullscreen mode

Test code:

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();
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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 to support the project! 🫰

For any questions, feel free to reach out via Telegram or email at yelaman.yelmuratov@gmail.com.

💖 💪 🙅 🚩
yelmuratoff
Yelaman Yelmuratov

Posted on May 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related