Testing and static analysis

Dart · Advanced · 12 min

Tests and lints are what keep a Dart codebase or Flutter app maintainable as it grows.

package:test is the standard test framework (Flutter's flutter_test builds on it). Tests live in test/ in files ending _test.dart, and run with dart test.

test('adds', () => expect(add(1, 2), 3));

Structure with group('name', () { ... }), and share fixtures with setUp / tearDown (per test) and setUpAll / tearDownAll.

expect(actual, matcher) accepts values or matchers: equals, isTrue, isNull, contains, hasLength, closeTo(3.14, 0.01), isA<FormatException>(), throwsA(...), throwsArgumentError, completion(...) for futures and emitsInOrder([...]) for streams.

Async tests just return a future: mark the body async and await. For code that throws, wrap the call: expect(() => parse('x'), throwsFormatException).

Run a subset with dart test --name 'parser' or a single file. Use package:mocktail or mockito for fakes, though a hand-written class that implements the dependency is often simpler.

Static analysis catches problems without running code: dart analyze reports errors, warnings and lints configured in analysis_options.yaml. Include package:lints/recommended.yaml (or flutter_lints) and add rules like unawaited_futures and prefer_final_locals. dart format gives one canonical style, and dart fix --apply auto-applies many suggestions. Run all three in CI.

Example

// test/cart_test.dart
import 'package:test/test.dart';
class Cart {
  final _items = <String, double>{};
  void add(String name, double price) {
    if (price < 0) throw ArgumentError.value(price, 'price');
    _items[name] = price;
  }
  double get total => _items.values.fold(0, (a, b) => a + b);
  Future<String> checkout() async => 'paid ${total.toStringAsFixed(2)}';
}
void main() {
  group('Cart', () {
    late Cart cart;
    setUp(() => cart = Cart()..add('tea', 2.5));
    test('sums prices', () {
      cart.add('cake', 0.1);
      expect(cart.total, closeTo(2.6, 1e-9));
    });
    test('rejects negative prices', () {
      expect(() => cart.add('x', -1), throwsArgumentError);
    });
    test('checks out', () async => expect(await cart.checkout(), 'paid 2.50'));
  });
}

Remember

  • test, group, setUp and expect with matchers
  • Async tests are async functions; wrap throwing calls in a closure
  • Run dart analyze, dart format and tests in CI

Watch out for

  • Writing expect(parse('x'), throwsA(...)) without the () => closure, so the exception is thrown before expect sees it.

Check yourself

  1. Why must you write `expect(() => f(), throwsStateError)` rather than `expect(f(), throwsStateError)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pub.dev/packages/test