# Flutter Testing

> 撰寫 Flutter 單元測試與 Widget 測試。當需要為新功能撰寫測試、驗證邏輯或測試 UI 元件時觸發。 Use when this capability is needed.

- Skill: `tomevault-io/flutter-testing` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/flutter-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/flutter-testing/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/flutter-testing

---


# Flutter 測試

## 何時使用

- 新增功能後撰寫測試
- 修改邏輯後驗證正確性
- 測試 Widget 行為與 UI 互動

## 測試結構

```
test/
├── data/
│   ├── database/
│   │   └── daos/
│   │       ├── media_dao_test.dart
│   │       └── playlist_dao_test.dart
│   ├── repositories/
│   │   └── media_repository_test.dart
│   └── services/
│       └── download_service_test.dart
├── presentation/
│   ├── providers/
│   │   └── player_state_provider_test.dart
│   └── widgets/
│       └── mini_player_test.dart
└── audio/
    └── audio_handler_test.dart
```

## 範例模式

### 單元測試（Repository / Service）

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

// Mock 類別
class MockMediaDao extends Mock implements MediaDao {}

void main() {
  late MediaRepository repository;
  late MockMediaDao mockDao;

  setUp(() {
    mockDao = MockMediaDao();
    repository = MediaRepository(mockDao);
  });

  group('MediaRepository', () {
    test('取得所有媒體項目', () async {
      // Arrange（準備）
      final items = [createTestMediaItem()];
      when(() => mockDao.getAllMediaItems()).thenAnswer((_) async => items);

      // Act（執行）
      final result = await repository.getAllMediaItems();

      // Assert（驗證）
      expect(result, equals(items));
      verify(() => mockDao.getAllMediaItems()).called(1);
    });

    test('切換我的最愛', () async {
      when(() => mockDao.toggleFavorite(any(), any()))
          .thenAnswer((_) async {});

      await repository.toggleFavorite('test-id', true);

      verify(() => mockDao.toggleFavorite('test-id', true)).called(1);
    });
  });
}
```

### Riverpod Provider 測試

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  test('playerProvider 初始狀態為未播放', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    final state = container.read(playerStateProvider);
    expect(state.isPlaying, isFalse);
    expect(state.currentItem, isNull);
  });
}
```

### Widget 測試

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

void main() {
  testWidgets('MiniPlayer 顯示目前曲目標題', (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          // 覆寫 Provider 提供測試資料
          currentMediaItemProvider.overrideWithValue(
            createTestMediaItem(title: '測試歌曲'),
          ),
        ],
        child: const MaterialApp(
          home: Scaffold(body: MiniPlayer()),
        ),
      ),
    );

    expect(find.text('測試歌曲'), findsOneWidget);
  });
}
```

## 常用指令

```bash
# 執行全部測試
flutter test

# 執行特定測試檔案
flutter test test/data/repositories/media_repository_test.dart

# 測試覆蓋率
flutter test --coverage
```

## 重要提醒

- 使用 `mocktail` 套件做 mock（比 `mockito` 更簡潔，不需 code generation）
- Riverpod Provider 測試使用 `ProviderContainer`
- Widget 測試需用 `ProviderScope` 包裝
- 測試檔案命名：`{原始檔名}_test.dart`
- 每個 public 方法至少一個正向 + 一個異常測試

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/rayliu1999) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-14 -->

