Approval Testing and why it’s important | Dart 🎯
Yelaman Yelmuratov
Posted on May 22, 2024
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:
- If the results match the approved file perfectly, the test passes.
- If there’s a difference, a reporter tool highlights the mismatch, and the test fails, providing a visual and textual representation of the changes.
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.
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
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:
- Set up a test: Import the Approval Tests library into your code.
- 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 theDiffReporter
to compare files in your IDE. The package currently supportsVS Code
andAndroid Studio
. If by some errors or other reason you want to make your own, there is a customDiffInfo option. - 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 inOptions
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.
-
DiffReporter
: Opens the Diff Tool in your IDE.
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}';
}
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();
}
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.
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.
Posted on May 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.