1. 前言
经过前面十篇的学习,你已经能够开发出一个功能完整的 Flutter 应用。但距离“交付”还差最后两步:确保质量和打包发布。
- 测试:自动验证代码行为,减少手动回归的负担。
- 发布:构建正式安装包(APK/IPA),并提交到 Google Play / App Store。
本篇将学习:
- 单元测试:测试 Dart 类的独立逻辑
- Widget 测试:测试组件的渲染和交互
- 集成测试(简介):模拟用户操作流
- 构建 Android 正式包(APK / App Bundle)
- 构建 iOS 正式包(IPA)
- 上架前的检查清单
2. Flutter 测试金字塔
Flutter 测试分为三层:
| 类型 | 测试对象 | 运行速度 | 编写难度 |
|---|---|---|---|
| 单元测试 | 单个函数/类 | 极快 | 简单 |
| Widget 测试 | 单个组件 | 快 | 中等 |
| 集成测试 | 完整应用或多模块 | 慢 | 复杂 |
日常开发中,应大量编写单元测试和 Widget 测试,少量关键路径编写集成测试。
3. 单元测试
3.1 添加依赖
Flutter 项目默认在 pubspec.yaml 的 dev_dependencies 中包含了 test 包。
dev_dependencies:
test: ^1.24.0
3.2 编写被测代码
以计数器模型的 Counter 类为例:
// lib/models/counter.dart
class Counter {
int _value = 0;
int get value => _value;
void increment() => _value++;
void decrement() => _value--;
}
3.3 编写测试文件
在 test/ 目录下创建 counter_test.dart。
import 'package:test/test.dart';
import 'package:my_app/models/counter.dart';
void main() {
group('Counter 测试', () {
test('初始值应为 0', () {
final counter = Counter();
expect(counter.value, 0);
});
test('increment 后值应加 1', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
test('decrement 后值应减 1', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
});
}
3.4 运行测试
在终端执行:
flutter test test/counter_test.dart
或者运行所有测试:
flutter test
4. Widget 测试
4.1 添加依赖
flutter_test 包已自动加入 dev_dependencies,无需额外添加。
4.2 测试一个简单的组件
假设有一个 Greeting 组件,根据 name 显示问候语。
// lib/widgets/greeting.dart
import 'package:flutter/material.dart';
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name!');
}
}
编写 Widget 测试:
// test/widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/greeting.dart';
void main() {
testWidgets('Greeting 应显示正确的名字', (WidgetTester tester) async {
// 构建组件
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Greeting(name: 'Flutter'),
),
),
);
// 查找 Text 组件并验证内容
expect(find.text('Hello, Flutter!'), findsOneWidget);
});
}
4.3 模拟点击和状态变化
测试一个带按钮的计数器组件(StatefulWidget)。
testWidgets('点击按钮应该增加计数', (WidgetTester tester) async {
await tester.pumpWidget(const MyCounterApp());
// 初始显示 0
expect(find.text('0'), findsOneWidget);
// 找到按钮并点击
final Finder button = find.byType(ElevatedButton);
await tester.tap(button);
await tester.pump(); // 重建界面
// 现在应显示 1
expect(find.text('1'), findsOneWidget);
});
关键 API:
pump():等待一帧重建pumpAndSettle():等待所有动画结束(适用复杂场景)tap()、drag()、enterText()模拟用户操作
5. 集成测试(简介)
集成测试用于验证应用整体功能,通常需要真机或模拟器运行。
5.1 添加依赖
dev_dependencies:
integration_test: ^1.0.0
flutter_test:
sdk: flutter
5.2 编写简单集成测试
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('启动应用并点击计数器', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
运行命令:
flutter test integration_test/app_test.dart
6. 构建正式包
6.1 Android 正式包
第一步:生成签名密钥
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
第二步:创建 android/key.properties,内容如下(不要提交到版本控制):
storePassword=yourStorePassword
keyPassword=yourKeyPassword
keyAlias=upload
storeFile=/Users/yourname/upload-keystore.jks
第三步:配置 android/app/build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
第四步:构建 APK 或 App Bundle
# 构建 APK(直接安装)
flutter build apk --release
# 构建 App Bundle(推荐上架 Google Play)
flutter build appbundle --release
产物位置:
- APK:
build/app/outputs/flutter-apk/app-release.apk - AAB:
build/app/outputs/bundle/release/app-release.aab
6.2 iOS 正式包
前提:需要一台 Mac 电脑,安装 Xcode,并加入 Apple Developer Program(年费 99 美元)。
第一步:在 Xcode 中配置签名
- 打开
ios/Runner.xcworkspace - 选择
Runner项目 →Signing & Capabilities→ 选择你的 Apple ID 和 Team
第二步:构建 IPA
flutter build ipa --release
产物位置:build/ios/ipa/Runner.ipa
之后可以使用 Transporter 或 Xcode Archive 上传到 App Store Connect。
7. 上架前的检查清单
在提交到应用商店之前,请核对以下事项:
通用
- 删除所有
debugPrint、print调试代码 - 关闭
debugPaintSizeEnabled等调试开关 - 检查应用的图标和启动图(splash screen)是否正确
- 测试在断网、低内存等极端情况下的行为
- 测试应用从后台恢复的行为
Android
android/app/build.gradle中的versionCode和versionName已更新- 已生成并配置签名密钥
- 已测试
--release包在真机上的运行 - 已创建 Google Play 开发者账号(25 美元一次性费用)
iOS
ios/Runner/Info.plist中权限描述完整(相机、相册等)- 应用图标已添加到
ios/Runner/Assets.xcassets - 已更新版本号(
CFBundleVersion和CFBundleShortVersionString) - 已在 TestFlight 中测试发布版
- 已加入 Apple Developer Program(年费 99 美元)
8. 常见问题与避坑
| 问题 | 原因 | 解决 |
|---|---|---|
单元测试找不到 test 包 | 未在 pubspec.yaml 中声明 | 检查 dev_dependencies |
| Widget 测试中找不到组件 | 组件可能被 Opacity、InheritedWidget 等包裹 | 使用 find.byKey 并给组件设置 Key |
| 构建 Android release 包时签名错误 | key.properties 路径或密码错误 | 检查文件路径,使用绝对路径 |
| iOS 构建时签名失败 | Xcode 未正确配置 Team | 在 Xcode 中重新选择 Team 并下载证书 |
| 应用上架被拒 | 缺少隐私政策、使用未声明的权限等 | 按商店指南完善描述 |
9. 小结与结语
本篇我们学习了:
- 单元测试、Widget 测试、集成测试的基本写法
- 构建 Android 正式包(APK/AAB)及签名配置
- 构建 iOS 正式包(IPA)及签名配置
- 上架前的检查清单
至此,Flutter 学习笔记系列已涵盖从环境搭建到应用发布的完整路径。