Approval Tests представляют собой альтернативный подход к традиционным утверждениям при тестировании программного обеспечения. Они особенно полезны при работе со сложными объектами, такими как длинные строки, коллекции или объекты с большим количеством свойств. Захватывая результат вывода и сравнивая его с утвержденной версией, утверждающие тесты упрощают процесс проверки того, что ваш код ведет себя так, как ожидается. В этой статье мы познакомимся с Dart-реализацией Approval Tests и продемонстрируем их использование на примере.
Что такое Approval Testing?
Тесты утверждения предлагают другой способ выполнения утверждений в тестах. Традиционные утверждения сравнивают результат непосредственно с ожидаемым значением, например:
expect(a, equals(b));
В отличие от них, тесты утверждений используют подход проверки:
Approvals.verify(a);
Вот как это работает:
Если результаты полностью совпадают с утвержденным файлом, тест пройден.
Если есть разница, инструмент reporter подчеркивает несоответствие, и тест не проходит, предоставляя визуальное и текстовое представление изменений.
![Running Approvals](https://habrastorage.org/getpro/habr/upload_files/b28/8d9/9ca/b288d99ca55963664e7d769f718d12e0.png)
Если вы исправляете ошибку (или добавляете функцию), это изменит ожидаемое поведение. Поэтому при запуске теста он будет провален.
![Add Behavior to Existing Approval](https://habrastorage.org/getpro/habr/upload_files/a9c/d9f/c2f/a9cd9fc2fdc70ac9fd7db8e831f00a62.png)
Зачем использовать Approval Tests?
Тесты утверждения упрощают цикл обратной связи, экономят время разработчиков, выделяя только то, что изменилось, а не требуя от них разбора всего вывода. Такой подход особенно полезен для сложных выводов, где традиционные утверждения могут быть громоздкими.
Dart реализация для Approval Tests
Чтобы использовать Approval Tests в своем проекте Dart, добавьте следующее в файл pubspec.yaml
:
dependencies:
approval_tests: ^1.0.0
Начало работы
Установите и откройте стартовый проект: Approvaltests.Dart.StarterProject
Этот проект включает:
Файл
.gitignore
для исключения артефактов одобрения.Линтер со всеми установленными правилами.
GitHub Actions для запуска тестов, при этом статус теста отображается на бейдже
README.md
.
Чтобы использовать Approval Tests:
Настройте тест: Импортируйте библиотеку Approval Tests в свой код.
Настройте репортер (по желанию): Репортеры выделяют различия между утвержденными и полученными файлами, когда тест проваливается. По умолчанию используется репортер
CommandLineReporter
. Вы также можете использоватьDiffReporter
для сравнения файлов в вашей IDE. В настоящее время пакет поддерживаетVS Code
иAndroid Studio
. Если по какой-то ошибке или другой причине вы хотите создать свой собственный, есть опцияcustomDiffInfo
.Управление файлом «approved»: Когда тест запускается в первый раз, автоматически создается файл одобрения. Этот файл представляет собой ожидаемый результат. Как только результаты теста станут удовлетворительными, обновите утвержденный файл, чтобы отразить эти изменения.
Утверждение результатов
Утверждение результатов заключается в сохранении файла .approved.txt
с желаемыми результатами. Распространенные подходы включают:
Использование diff tools для перемещения текста слева направо и сохранения результата.
Использование свойства
approveResult
вOptions
для автоматического утверждения после выполнения теста.Переименование файла
.received
в.approved
.
Репортеры
Репортеры запускают инструменты diff, когда что-то не совпадает, что позволяет легко увидеть изменения. Доступные репортеры включают:
CommandLineReporter
: Выводит diff в терминал.
![CommandLineReporter](https://habrastorage.org/getpro/habr/upload_files/053/c6e/1b8/053c6e1b8e7a7ceaa36ce18d589a6817.png)
DiffReporter
: Открывает инструмент Diff Tool в вашей IDE.
![VS Code DiffReporter](https://habrastorage.org/getpro/habr/upload_files/5d5/600/462/5d56004624b3fabeb9f1049ca2ac0479.png)
Пример: Ката «Gilded Rose»
Следующий пример демонстрирует, как использовать Approval Tests для рефакторинга кода GildedRose
из 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}';
}
Код теста:
import 'package:approval_tests/approval_tests.dart';
import 'package:starter_project/starter_project.dart';
import 'package:test/test.dart';
void main() {
// Определяем все случаи
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,
);
});
});
}
// Функция для обработки каждой комбинации и создания вывода для проверки
String processItemCombination(Iterable<List<dynamic>> combinations) {
final receivedBuffer = StringBuffer();
for (final combination in combinations) {
final String itemName = combination[0] as String;
final int sellIn = combination[1] as int;
final int quality = combination[2] as int;
// Создаем объект Item, представляющий текущую комбинацию
final Item testItem = Item(itemName, sellIn: sellIn, quality: quality);
final GildedRose app = GildedRose(items: [testItem]);
// Изменение качества testItem
app.updateQuality();
// Добавление обновленного элемента в ожидаемые Items
receivedBuffer.writeln(testItem.toString());
}
// Возвращаем строковое представление обновленного элемента
return receivedBuffer.toString();
}
И на выходе мы получаем 120 различных комбинаций, на основе которых можно приступать к рефакторингу кода. Конечно, можно разделить генерацию комбинаций на части и убрать ненужные, но это лишь пример.
![120 комбинаций для Gilded Rose Kata 120 комбинаций для ката «Позолоченная роза»](https://habrastorage.org/getpro/habr/upload_files/048/856/2f8/0488562f80d81622dd32a9bca7550573.png)
Заключение
Approval Tests для Dart - это отличная альтернатива традиционным утверждениям, особенно для сложных объектов и выводов. Упрощая процесс проверки, они помогают разработчикам сосредоточиться на том, что изменилось, сокращая время и усилия, необходимые для понимания влияния изменений в коде. Дополнительную информацию, примеры и способы участия можно найти в репозитории Approval Tests Dart.
Прошу обратить внимание, что это моя первая статья и первая публичная библиотека. Я активно общаюсь с создателем Approval Tests Ллевелином Фалько по поводу улучшений библиотеки. Ваши предложения, проблемы и запросы на исправление очень приветствуются. Давайте вместе сделаем эту библиотеку еще лучше! 🙌
Поставьте звезду на репозиторий, чтобы поддержать проект! 🫰
По всем вопросам обращайтесь через Telegram или по электронной почте yelaman.yelmuratov@gmail.com.