Dart: Dart Testing — From Unit Tests to Integration Tests

Last updated: 2026-08-26

Code without tests is a ticking time bomb — testing is an insurance policy for your future self.

1. What You Will Learn


2. A Developer's Real Story

(1) Pain Point: Silent Breakage After Refactoring

Charlie refactored the amount calculation logic in DataPipeline, changing the tax rate from a fixed value to a query based on region. After local testing passed and deployment, the refactoring unexpectedly affected the discount stacking calculation — the discount amount became negative, causing incorrect refund amount calculations for 5,000 orders, leading to a direct loss of 25,000 USD. If there had been automated tests, this bug would have been caught before deployment.

(2) The Solution: A Testing System

Dart's test package provides a comprehensive testing framework. group organizes the test structure, expect + Matchers provide rich assertions, and setUp/tearDown manage the test environment.

DART
group('Order calculation', () {
  test('applies tax correctly', () {
    final order = Order(id: 'T1', amount: 1000.0);
    expect(order.calcTax(0.08), equals(80.0));
  });

  test('discount does not make total negative', () {
    final order = Order(id: 'T2', amount: 100.0, discountRate: 0.5);
    expect(order.total, greaterThan(0));
  });
});
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

(3) Benefits


3. Test Package Fundamentals

(1) Core Syntax

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Basic Test

DART
// test/order_test.dart
import 'package:test/test.dart';

void main() {
  test('simple addition', () {
    expect(2 + 3, equals(5));
  });

  test('string contains', () {
    expect('DataPipeline v1.0', contains('Pipeline'));
  });

  test('list is not empty', () {
    expect([1, 2, 3], isNotEmpty);
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Organizing Tests with group

DART
import 'package:test/test.dart';

double calculateTax(double amount, double rate) => amount * rate;
double calculateTotal(double amount, double taxRate, double discountRate) {
  final discounted = amount * (1 - discountRate);
  return discounted * (1 + taxRate);
}

void main() {
  group('Tax calculation', () {
    test('standard rate', () {
      expect(calculateTax(1000.0, 0.08), equals(80.0));
    });

    test('zero rate', () {
      expect(calculateTax(1000.0, 0.0), equals(0.0));
    });

    test('high rate', () {
      expect(calculateTax(1000.0, 0.25), equals(250.0));
    });
  });

  group('Total calculation', () {
    test('no discount', () {
      expect(calculateTotal(1000.0, 0.08, 0.0), equals(1080.0));
    });

    test('with discount', () {
      expect(calculateTotal(1000.0, 0.08, 0.1), closeTo(972.0, 0.01));
    });

    test('full discount', () {
      expect(calculateTotal(1000.0, 0.08, 1.0), equals(0.0));
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

(2) Common Matchers

Matcher Meaning Example
equals(value) Equals equals(5)
greaterThan(n) Greater than greaterThan(0)
lessThan(n) Less than lessThan(100)
closeTo(num, delta) Approximately equal closeTo(3.14, 0.01)
contains(item) Contains contains('key')
isEmpty Is empty isEmpty
isNotEmpty Is not empty isNotEmpty
isNull Is null isNull
isNotNull Is not null isNotNull
isTrue / isFalse Boolean check isTrue
throwsException Throws an exception throwsException
isA``<Type>``() Type check isA``<String>``()

4. setUp / tearDown

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Managing the Test Environment

DART
import 'package:test/test.dart';

class OrderBook {
  final List``<double>`` orders = [];

  void add(double amount) {
    if (amount <= 0) throw ArgumentError('Amount must be positive');
    orders.add(amount);
  }

  double get total => orders.fold(0.0, (a, b) => a + b);
  double get average => orders.isEmpty ? 0 : total / orders.length;
  int get count => orders.length;
}

void main() {
  late OrderBook book;

  setUp(() {
    // Runs before each test
    book = OrderBook();
  });

  tearDown(() {
    // Runs after each test
    // Cleanup if needed
  });

  group('OrderBook', () {
    test('starts empty', () {
      expect(book.count, equals(0));
      expect(book.total, equals(0.0));
    });

    test('add increases count', () {
      book.add(100.0);
      expect(book.count, equals(1));
      book.add(200.0);
      expect(book.count, equals(2));
    });

    test('total sums amounts', () {
      book.add(1500.0);
      book.add(3200.0);
      expect(book.total, equals(4700.0));
    });

    test('rejects negative amount', () {
      expect(() => book.add(-50.0), throwsArgumentError);
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

5. Unit Testing

(1) Principles of Isolated Testing

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Unit Testing Functions

DART
import 'package:test/test.dart';

// Functions to test
double parseAmount(String input) {
  final value = double.tryParse(input);
  if (value == null) throw FormatException('Invalid amount: $input');
  if (value <= 0) throw ArgumentError('Amount must be positive');
  return value;
}

String formatUSD(double amount) => '\$${amount.toStringAsFixed(2)} USD';

String classifyAmount(double amount) => switch (amount) {
  >= 10000 => 'Enterprise',
  >= 1000 => 'Premium',
  > 0 => 'Standard',
  _ => throw ArgumentError('Invalid amount'),
};

void main() {
  group('parseAmount', () {
    test('parses valid integer', () {
      expect(parseAmount('1500'), equals(1500.0));
    });

    test('parses valid decimal', () {
      expect(parseAmount('99.99'), closeTo(99.99, 0.001));
    });

    test('throws on invalid input', () {
      expect(() => parseAmount('abc'), throwsFormatException);
    });

    test('throws on negative', () {
      expect(() => parseAmount('-50'), throwsArgumentError);
    });

    test('throws on zero', () {
      expect(() => parseAmount('0'), throwsArgumentError);
    });
  });

  group('formatUSD', () {
    test('formats whole number', () {
      expect(formatUSD(1500), equals('\$1500.00 USD'));
    });

    test('formats decimal', () {
      expect(formatUSD(99.99), equals('\$99.99 USD'));
    });
  });

  group('classifyAmount', () {
    test('Enterprise tier', () {
      expect(classifyAmount(15000), equals('Enterprise'));
    });

    test('Premium tier', () {
      expect(classifyAmount(1500), equals('Premium'));
    });

    test('Standard tier', () {
      expect(classifyAmount(50), equals('Standard'));
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

6. Mock and fake

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Manual Fake

DART
import 'package:test/test.dart';

// Interface to mock
abstract class DataSource {
  Future<List``<String>``> fetchIds();
  Future``<double>`` fetchAmount(String id);
}

// Manual fake implementation
class FakeDataSource implements DataSource {
  final List``<String>`` _ids;
  final Map<String, double> _amounts;

  FakeDataSource({List``<String>``? ids, Map<String, double>? amounts})
      : _ids = ids ?? ['ORD-001', 'ORD-002'],
        _amounts = amounts ?? {'ORD-001': 1500.0, 'ORD-002': 3200.0};

  @override
  Future<List``<String>``> fetchIds() async => _ids;

  @override
  Future``<double>`` fetchAmount(String id) async =>
      _amounts[id] ?? throw Exception('Not found: $id');
}

// Service under test
class OrderService {
  final DataSource _source;

  OrderService(this._source);

  Future``<double>`` getTotalRevenue() async {
    final ids = await _source.fetchIds();
    double total = 0;
    for (final id in ids) {
      total += await _source.fetchAmount(id);
    }
    return total;
  }
}

void main() {
  group('OrderService', () {
    test('calculates total revenue', () async {
      final fakeSource = FakeDataSource(
        ids: ['ORD-001', 'ORD-002'],
        amounts: {'ORD-001': 1500.0, 'ORD-002': 3200.0},
      );
      final service = OrderService(fakeSource);

      expect(await service.getTotalRevenue(), equals(4700.0));
    });

    test('handles empty data source', () async {
      final fakeSource = FakeDataSource(ids: [], amounts: {});
      final service = OrderService(fakeSource);

      expect(await service.getTotalRevenue(), equals(0.0));
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

7. Bob's Scenario: DataPipeline Test Coverage

▶ Example

TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

: Core Logic Testing

DART
import 'package:test/test.dart';

class Order {
  final String id;
  final double amount;
  final String status;
  final String category;
  final double? discountRate;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    required this.category,
    this.discountRate,
  }) {
    if (amount <= 0) throw ArgumentError('Amount must be positive: $amount');
    if (id.isEmpty) throw ArgumentError('ID cannot be empty');
  }

  double get effectiveDiscount => discountRate ?? 0;
  double get discountedAmount => amount * (1 - effectiveDiscount);
  double get tax => discountedAmount * 0.08;
  double get total => discountedAmount + tax;
}

class OrderAnalyzer {
  static double totalRevenue(List``<Order>`` orders) =>
      orders.fold(0.0, (sum, o) => sum + o.total);

  static Map<String, double> revenueByCategory(List``<Order>`` orders) {
    final result = <String, double>{};
    for (final o in orders) {
      result.update(o.category, (v) => v + o.total, ifAbsent: () => o.total);
    }
    return result;
  }

  static List``<Order>`` filterCompleted(List``<Order>`` orders) =>
      orders.where((o) => o.status == 'completed').toList();
}

void main() {
  group('Order', () {
    test('calculates tax correctly', () {
      final order = Order(id: 'T1', amount: 1000.0, status: 'completed', category: 'E');
      expect(order.tax, closeTo(80.0, 0.01));
    });

    test('calculates total with discount', () {
      final order = Order(id: 'T2', amount: 1000.0, status: 'completed', category: 'E', discountRate: 0.1);
      expect(order.total, closeTo(972.0, 0.01));
    });

    test('rejects negative amount', () {
      expect(() => Order(id: 'T3', amount: -100, status: 'completed', category: 'E'),
          throwsArgumentError);
    });

    test('rejects empty ID', () {
      expect(() => Order(id: '', amount: 100, status: 'completed', category: 'E'),
          throwsArgumentError);
    });
  });

  group('OrderAnalyzer', () {
    late List``<Order>`` testOrders;

    setUp(() {
      testOrders = [
        Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
        Order(id: 'ORD-002', amount: 3200.0, status: 'pending', category: 'Electronics'),
        Order(id: 'ORD-003', amount: 890.0, status: 'completed', category: 'Clothing'),
      ];
    });

    test('filters completed orders', () {
      final completed = OrderAnalyzer.filterCompleted(testOrders);
      expect(completed.length, equals(2));
      expect(completed.every((o) => o.status == 'completed'), isTrue);
    });

    test('calculates total revenue', () {
      final revenue = OrderAnalyzer.totalRevenue(testOrders);
      expect(revenue, greaterThan(0));
    });

    test('groups revenue by category', () {
      final byCategory = OrderAnalyzer.revenueByCategory(testOrders);
      expect(byCategory.containsKey('Electronics'), isTrue);
      expect(byCategory.containsKey('Clothing'), isTrue);
    });

    test('handles empty list', () {
      expect(OrderAnalyzer.totalRevenue([]), equals(0.0));
      expect(OrderAnalyzer.filterCompleted([]), isEmpty);
      expect(OrderAnalyzer.revenueByCategory([]), isEmpty);
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

8. Complete Example: DataPipeline Test Suite

DART
// ============================================
// DataPipeline Test Suite
// Comprehensive testing with groups, setup, and fakes
// ============================================

import 'package:test/test.dart';

// Production code
class PipelineConfig {
  final int batchSize;
  final double taxRate;
  final String outputFormat;

  PipelineConfig({
    this.batchSize = 10000,
    this.taxRate = 0.08,
    this.outputFormat = 'json',
  });

  PipelineConfig copyWith({int? batchSize, double? taxRate, String? outputFormat}) =>
      PipelineConfig(
        batchSize: batchSize ?? this.batchSize,
        taxRate: taxRate ?? this.taxRate,
        outputFormat: outputFormat ?? this.outputFormat,
      );
}

class Order {
  final String id;
  final double amount;
  final String status;
  final String category;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    required this.category,
  }) {
    if (id.isEmpty) throw ArgumentError('Empty order ID');
    if (amount <= 0) throw ArgumentError('Amount must be positive');
  }
}

class ReportGenerator {
  final PipelineConfig config;

  ReportGenerator(this.config);

  Map<String, dynamic> generate(List``<Order>`` orders) {
    final completed = orders.where((o) => o.status == 'completed').toList();
    final revenue = completed.fold``<double>``(0, (s, o) => s + o.amount);
    final tax = revenue * config.taxRate;

    final byCategory = <String, int>{};
    for (final o in completed) {
      byCategory.update(o.category, (v) => v + 1, ifAbsent: () => 1);
    }

    return {
      'totalOrders': orders.length,
      'completedOrders': completed.length,
      'revenue': revenue,
      'tax': tax,
      'totalWithTax': revenue + tax,
      'byCategory': byCategory,
      'format': config.outputFormat,
    };
  }
}

// Tests
void main() {
  group('PipelineConfig', () {
    test('has default values', () {
      final config = PipelineConfig();
      expect(config.batchSize, equals(10000));
      expect(config.taxRate, equals(0.08));
      expect(config.outputFormat, equals('json'));
    });

    test('copyWith preserves unspecified fields', () {
      final original = PipelineConfig(batchSize: 50000);
      final modified = original.copyWith(taxRate: 0.10);
      expect(modified.batchSize, equals(50000));
      expect(modified.taxRate, equals(0.10));
    });
  });

  group('Order', () {
    test('creates valid order', () {
      final order = Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'E');
      expect(order.id, equals('ORD-001'));
      expect(order.amount, equals(1500.0));
    });

    test('rejects empty ID', () {
      expect(() => Order(id: '', amount: 100, status: 'ok', category: 'E'),
          throwsArgumentError);
    });

    test('rejects non-positive amount', () {
      expect(() => Order(id: 'X', amount: 0, status: 'ok', category: 'E'),
          throwsArgumentError);
      expect(() => Order(id: 'X', amount: -1, status: 'ok', category: 'E'),
          throwsArgumentError);
    });
  });

  group('ReportGenerator', () {
    late PipelineConfig config;
    late ReportGenerator generator;
    late List``<Order>`` testOrders;

    setUp(() {
      config = PipelineConfig(taxRate: 0.08);
      generator = ReportGenerator(config);
      testOrders = [
        Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
        Order(id: 'ORD-002', amount: 50.0, status: 'completed', category: 'Books'),
        Order(id: 'ORD-003', amount: 3200.0, status: 'pending', category: 'Electronics'),
      ];
    });

    test('counts total and completed orders', () {
      final report = generator.generate(testOrders);
      expect(report['totalOrders'], equals(3));
      expect(report['completedOrders'], equals(2));
    });

    test('calculates revenue from completed only', () {
      final report = generator.generate(testOrders);
      expect(report['revenue'], equals(1550.0));
    });

    test('applies tax rate from config', () {
      final report = generator.generate(testOrders);
      expect(report['tax'], closeTo(124.0, 0.01));
    });

    test('groups by category', () {
      final report = generator.generate(testOrders);
      final byCategory = report['byCategory'] as Map<String, int>;
      expect(byCategory['Electronics'], equals(1));
      expect(byCategory['Books'], equals(1));
    });

    test('handles empty order list', () {
      final report = generator.generate([]);
      expect(report['totalOrders'], equals(0));
      expect(report['revenue'], equals(0.0));
    });

    test('respects output format config', () {
      final jsonGen = ReportGenerator(PipelineConfig(outputFormat: 'json'));
      final csvGen = ReportGenerator(PipelineConfig(outputFormat: 'csv'));
      expect(jsonGen.generate(testOrders)['format'], equals('json'));
      expect(csvGen.generate(testOrders)['format'], equals('csv'));
    });
  });
}
TEXT 📖 Display only
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.

❓ FAQ

Q: Where should test files be placed? A: In the test/ folder at the project root, with filenames ending in _test.dart. Running dart test automatically discovers all tests.

Q: What's the difference between unit tests and integration tests? A: Unit tests isolate and test a single function/class without relying on external resources. Integration tests verify that multiple modules collaborate correctly and may involve files, databases, or networks.

Q: Can setUp be nested within groups? A: Yes. The outer group's setUp runs first, followed by the inner one's. This is suitable for sharing basic settings + custom configuration per group.

Q: Should I choose mockito or a manual fake? A: For simple interfaces, use a manual fake (less code, type-safe). For complex interfaces or when you need to verify call counts, use mockito. Dart recommends prioritizing manual fakes.

Q: How do I test asynchronous code? A: The test callback supports async, so use test('...', () async { ... }) directly. expect also supports Futures. Don't forget to await.

Q: How do I check test coverage? A: Run dart test --coverage=coverage, then use the format_coverage tool from the coverage package to generate a report.

Q: How do I test code that throws exceptions? A: Use expect(() => someFunction(), throwsException) or more specific variants like throwsFormatException, throwsArgumentError, etc.


📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Write 5 unit tests for a formatUSD(double amount) function: normal amount, zero, negative, very large number, and decimal precision.
  2. Intermediate (Difficulty ⭐⭐): Create a FakeApiService implementing the DataSource interface. Use it to test the getTotalRevenue() and getOrderById() methods of OrderService. Include both success and failure scenarios.
  3. Advanced (Difficulty ⭐⭐⭐): Write a complete test suite for DataPipeline's ReportGenerator: use setUp to share test data, group tests by functionality, test edge cases (empty list, single item, all pending status), with a coverage target of ≥ 90%.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏