60  Unit Testing

I’ll create a comprehensive tutorial on writing unit tests in Dart that’s tailored to your programming experience. Since you’re familiar with testing concepts from Python and R, I’ll focus on Dart-specific patterns and best practices.

60.1 Unit Testing in Dart: A Quick Tutorial

Unit testing in Dart is straightforward and follows patterns similar to other testing frameworks you might know from Python (like pytest or unittest).

60.1.1 Setting Up Testing

First, add the test package to your pubspec.yaml:

dev_dependencies:
  test: ^1.24.0

60.1.2 Basic Test Structure

Create test files in the test/ directory with _test.dart suffix:

// test/calculator_test.dart
import 'package:test/test.dart';
import '../lib/calculator.dart';

void main() {
  test('Calculator adds two numbers', () {
    final calculator = Calculator();
    expect(calculator.add(2, 3), equals(5));
  });
}

60.1.3 Key Testing Components

The main components you’ll use:

  • test() - Defines a single test case
  • group() - Groups related tests (like Python’s test classes)
  • expect() - Makes assertions (similar to assert in Python)
  • setUp() / tearDown() - Run before/after each test

60.1.4 Example: Testing a Simple Class

Let’s create a more complete example. First, the class to test:

// lib/user.dart
class User {
  final String name;
  final int age;
  
  User({required this.name, required this.age});
  
  bool get isAdult => age >= 18;
  
  String greet() => 'Hello, my name is $name';
  
  User birthday() => User(name: name, age: age + 1);
}

Now the comprehensive test:

// test/user_test.dart
import 'package:test/test.dart';
import '../lib/user.dart';

void main() {
  group('User', () {
    late User user;
    
    setUp(() {
      // Runs before each test
      user = User(name: 'Alice', age: 25);
    });
    
    test('should create user with correct properties', () {
      expect(user.name, equals('Alice'));
      expect(user.age, equals(25));
    });
    
    test('should identify adult correctly', () {
      expect(user.isAdult, isTrue);
      
      final teenager = User(name: 'Bob', age: 16);
      expect(teenager.isAdult, isFalse);
    });
    
    test('should greet properly', () {
      expect(user.greet(), equals('Hello, my name is Alice'));
    });
    
    test('birthday should increment age', () {
      final olderUser = user.birthday();
      expect(olderUser.age, equals(26));
      expect(olderUser.name, equals('Alice'));
    });
  });
}

60.1.5 Common Matchers

Dart provides many matchers for assertions:

// Equality
expect(actual, equals(expected));
expect(actual, expected); // shorthand

// Boolean
expect(value, isTrue);
expect(value, isFalse);

// Null
expect(value, isNull);
expect(value, isNotNull);

// Types
expect(value, isA<String>());
expect(value, isA<List<int>>());

// Numbers
expect(value, greaterThan(10));
expect(value, lessThanOrEqualTo(20));
expect(value, closeTo(3.14, 0.01)); // within tolerance

// Collections
expect(list, contains('item'));
expect(list, hasLength(3));
expect(list, isEmpty);
expect(list, isNotEmpty);

// Exceptions
expect(() => dangerousCode(), throwsException);
expect(() => divideByZero(), throwsA(isA<ArgumentError>()));

60.1.6 Testing Async Code

For async operations (similar to Python’s async testing):

test('async operation completes', () async {
  final future = fetchData();
  await expectLater(future, completes);
});

test('async function returns correct value', () async {
  final result = await fetchUserName();
  expect(result, equals('Alice'));
});

test('stream emits values', () async {
  final stream = countStream();
  expect(stream, emitsInOrder([1, 2, 3]));
});

60.1.7 Running Tests

Run tests from the command line:

# Run all tests
dart test

# Run specific test file
dart test test/user_test.dart

# Run with coverage
dart test --coverage=coverage

# Watch mode (re-runs on file changes)
dart test --reporter compact --watch

60.1.8 Best Practices

  1. Test Organization:

    project/
    ├── lib/
    │   ├── src/
    │   │   └── calculator.dart
    │   └── user.dart
    └── test/
        ├── src/
        │   └── calculator_test.dart
        └── user_test.dart
  2. Descriptive Test Names:

    test('should throw ArgumentError when dividing by zero', () {
      // Better than: test('division test', () {
    });
  3. Arrange-Act-Assert Pattern:

    test('should calculate discount correctly', () {
      // Arrange
      final product = Product(price: 100);
    
      // Act
      final discountedPrice = product.applyDiscount(0.2);
    
      // Assert
      expect(discountedPrice, equals(80));
    });

60.1.9 Flutter-Specific Testing

For Flutter widgets, you’ll use flutter_test:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

void main() {
  testWidgets('Counter increments', (WidgetTester tester) async {
    // Build widget
    await tester.pumpWidget(MyApp());
    
    // Find widgets
    expect(find.text('0'), findsOneWidget);
    
    // Interact
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    // Verify
    expect(find.text('1'), findsOneWidget);
  });
}

60.1.10 Try It Yourself

Create a simple StringUtils class and write tests for it:

// lib/string_utils.dart
class StringUtils {
  static String capitalize(String text) {
    if (text.isEmpty) return text;
    return text[0].toUpperCase() + text.substring(1).toLowerCase();
  }
  
  static int wordCount(String text) {
    return text.trim().split(RegExp(r'\s+')).length;
  }
}