单元测试
- 单元测试是针对一个函数或者类进行测试;
- 第一步:添加测试依赖
- 将 test 或者 flutter_test加入依赖文件,默认创建的Flutter程序已经有了依赖;
- Test 包提供了编写测试所需要的核心功能;
dev_dependencies:
flutter_test:
sdk: flutter
- 第二步:创建需要测试的类SFCounter
class SFCounter {
int value = 0;
int intcrement() => value++;
int decrement() => value--;
}
第三步:创建测试文件counter_test
,测试代码如下:
import 'package:TestDemo/counter.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group("counter class test", () {
SFCounter counter;
setUpAll(() {
counter = SFCounter();
});
test("counter test", () {
expect(counter.value, 0);
});
test("counter increment method", () {
counter.intcrement();
expect(counter.value, 1);
});
test("counter decrement method", () {
counter.decrement();
expect(counter.value, 0);
});
});
}
-
group
函数,可同时测试多个函数, - 测试结果如下所示:
- 现在我手动将逻辑改错,就会出现测试不通过的场景,如下所示:
Widget测试
- Widget测试主要是针对某一个封装的Widget进行单独测试;
- 第一步:同样要添加测试依赖,与单元测试的依赖相同,默认已经添加;
- 第二步:创建测试Widget -> SFContacts
import 'package:flutter/material.dart';
class SFContacts extends StatelessWidget {
final List<String> _names;
SFContacts(this._names);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("联系人列表"),
),
body: ListView(
children: _names.map((name) {
return ListTile(
leading: Icon(Icons.people),
title: Text(name),
);
}).toList(),
),
);
}
}
- 第三步:创建测试文件
contacts_widget_test
,代码入下:
import 'package:TestDemo/contacts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("test Contacts widget", (WidgetTester tester) async {
//注入widget
await tester.pumpWidget(MaterialApp(home: SFContacts(["aaa","bbb","ccc"])));
//在SFContacts查找widget
final aaaText = find.text("aaa");
final bbbText = find.text("bbb");
final cccText = find.text("ccc");
final icons = find.byIcon(Icons.people);
//断言
expect(aaaText, findsOneWidget);
expect(bbbText, findsOneWidget);
expect(cccText, findsOneWidget);
expect(icons, findsNWidgets(3));
});
}
-
testWidgets
:flutter_test中用于测试Widget的函数; -
tester.pumpWidget
:pumpWidget 方法会建立并渲染我们提供的 widget; -
find
:find() 方法来创建我们的 Finders;- findsNothing:验证没有可被查找的 widgets;
- findsWidgets:验证一个或多个 widgets 被找到;
- findsNWidgets:验证特定数量的 widgets 被找到;
- 官方文档:https://flutter.dev/docs/cookbook/testing/widget/tap-drag
集成测试
-
单元测试和Widget测试都是在测试独立的类或函数或Widget,它们并不能测试单独的模块形成的整体或者获取真实设备或模拟器上应用运行的状态;这些测试任务可以交给 集成测试 来完成;
-
集成测试的两个大的步骤:
- 发布一个可测试应用程序到真实设备或者模拟器上;
- 利用独立的测试套件去驱动应用程序,检查仪器是否完好可用;
-
第一步:创建可测试应用程序
-
就是利用官方的默认生成的代码,然后在Text与FloatingActionButton两个组件分别绑定key,
textKey
与buttonKey
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
key: ValueKey("counter"),
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
key: ValueKey("increment"),
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
- 第二步:添加flutter_driver依赖,需要用到 flutter_driver 包来编写 集成测试;
dev_dependencies:
flutter_driver:
sdk: flutter
flutter_test:
sdk: flutter
test: any
- 第三步:创建测试文件,代码如下:
- 和单元测试以及Widget测试不同的是,集成测试的程序和待测试的应用并不在同一个进程内,所以我们通常会创建两个文件:
- 文件一(app.dart):用于启动带测试的应用程序;
- 文件二(app_test.dart):编写测试的代码;
- 我们可以将这两个文件放到一个文件中:test_driver
- 第四步:编写安装应用代码 app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:TestDemo/main.dart' as app;
void main() {
//1.初始化Driver
enableFlutterDriverExtension();
//2.启动应用程序
app.main();
}
- 第五步:编写集成测试代码 app_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group("counter application test", () {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() {
driver.close();
});
final textFinder = find.byValueKey("textKey");
final buttonFinder = find.byValueKey("buttonKey");
test("test default value", () async {
expect(await driver.getText(textFinder), "0");
});
test("floatingActionButton click", () async {
await driver.tap(buttonFinder);
expect(await driver.getText(textFinder), "1");
});
});
}
- 第六步:运行集成测试
- 首先,启动安卓模拟器或者 iOS 模拟器,或者直接把 iOS 或 Android 真机连接到你的电脑上;
- 然后,在项目的根文件夹下 终端执行
flutter drive --target=test_driver/app.dart
- 这个指令的作用:
- 创建 --target 目标应用并且把它安装在模拟器或真机中;
- 启动应用程序;
- 运行位于 test_driver/ 文件夹下的 app_test.dart 测试套件;
- 运行结果:我们会发现正常运行,并且结果app中的FloatingActionButton自动被点击了一次;
编译
- 在Android和iOS中,应用程序运行分为debug和release模式,分别对应调试阶段和发布阶段;
- 在Flutter中,应用程序分为debug、profile、release三种模式;
debug模式
-
在 Debug 模式下,app 可以被安装在真机、模拟器、仿真器上进行调试;
-
debug模式的特点:
- 断言是开启的(Assertions)
- 服务扩展是开启的(Service extension)
- 这个可以从runApp的源码查看
- runApp -> WidgetsFlutterBinding -> initServiceExtensions
- 开启调试,类似于DevTools的工具可以连接到应用程序的进程中
- 针对快速开发和运行周期进行了编译优化(但不是针对执行速度、二进制文件大小或者部署),比如Dart是JIT模式(Just In Time,即时编译,也可以理解成 边运行边编译)
-
默认情况下,运行 flutter run 会使用 Debug 模式,点击Android Studio run按钮,也是debug模式;
-
下面的情况会出现在Debug 模式下:
-热重载(Hot Reload)功能仅能在调试模式下运行;- 仿真器和模拟器仅能在调试模式下运行;
- 在debug模式下,应用可能会出现掉帧或者卡顿现象;
release模式
- 当我们要发布应用程序时,总是希望最大化的优化性能和应用程序所占据的空间;
- 在 Release 模式下是不支持模拟器和仿真器的,只能在真机上运行;
- Release 模式有如下特点:
- 断言是无效的;
- 服务扩展是无效的;
- debugging是无效的;
- 编译针对快速启动、快速执行和小的 package 的大小进行了优化;
profile模式
- profile模式和release模式类似,但是会保留一些信息方便我们对性能进行检测;
- profile模式有如下特点:
- 保留了一些扩展是开启的;
- DevTools的工具可以连接到应用程序的进程中;
- Profile模式最重要的作用就是可以利用DevTools来测试应用的性能;
开发中模式区分
- 第一种方式:通过断言
assert
来区分,因为在release模式下断言是无效的;
String baseURL = "production baseURL";
assert(() {
baseURL = "development baseURL";
return true;
}());
-
在debug模式下,会立即执行断言中的 函数,给baseURL赋值,而在release模式下,直接忽略断言;
-
第二种方式:通过kReleaseMode常量来区分
String baseURL = kReleaseMode ? "production baseURL": "development baseURL";
-
kReleaseMode
是package:flutter/foundation.dart
中定义的一个常量; -
当然,上面只是针对baseURL来进行了区分,开发中如果有多个属性需要区分呢?
-
可以封装一个Config的类,通过InheritedWidget来进行共享即可;
打包发布
Android打包和发布
第一步:填写应用配置
应用的AppID
应用的名称
应用的Icon
应用的Launcher
-
版本信息
,在pubspec.yaml中,如下所示:
- 在Android中,应用的版本分为versionCode & versionName
- versionCode:内部管理的版本号
- versionName:用户显示的版本号
- 在iOS中,应用的版本分为 version & build
- version:用户显示的版本
- build:内部管理的版本
- 在Flutter中,管理这两个版本号:
- 1.0.0.0:用户显示的版本
- 1:内部管理的版本
-
用户权限配置
: - 在Android中某些用户权限需要在AndroidManifest.xml进行配置:
- 比如默认情况下应用程序是不能发送网络请求的,如果之后App中有用到网络请求,那么需要在AndroidManifest.xml中进行如下配置(默认debug模式下有配置网络请求);
- 比如我们需要访问用户的位置,那么需要在AndroidManifest.xml中进行如下配置;
第二步:应用程序签名
-
Android系统在安装APK的时候,首先会检验APK的签名,如果发现签名文件不存在或者校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名;
-
创建一个秘钥库
,Mac终端执行keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
,Mac必须安装Java运行环境,否则执行失败,按照提示回答相关问题,最后生成一个.jks文件
; -
在app中引用秘钥库
:在Android的根目录下创建一个key.properties
文件,它包含了密钥库位置的定义:
storePassword=<上一步骤中的密码>
keyPassword=<上一步骤中的密码>
keyAlias=key
storeFile=<密钥库的位置,e.g. /Users/<用户名>/key.jks>
image.png
- 注意:这个文件一般不要提交到代码仓库,修改.gitignore文件,如下:
# Android ignore
/android/key.properties
-
在gradle中配置签名
: - 通过编辑 /android/app/build.gradle 文件来为我们的 app 配置签名:
- 首先:在Android代码块之前添加:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
image.png
-
将 key.properties 文件加载到 keystoreProperties 对象中
-
其次:替换android中的 buildTypes 代码块
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Snip20211105_42.png
- 现在我们发布的app就会被
自动签名
了;
第三步:打包应用程序
- 目前Android支持打包两种应用程序:APK、AAB;
- APK文件:
- Android application package
- 目前几乎所有的应用市场都支持上传APK文件
- 用户直接安装的就是APK文件
- 终端执行
flutter build apk
- AAB文件:
- Android App Bundle
- Google推出的一种新的上传格式,某些应用市场不支持的
- 会根据用户打包的aab文件,动态生成用户设备需要的APK文件
- 终端执行
flutter build appbundle
第四步:发布应用程序
- Android应用程序可以发布到很多的平台,包括国内的平台和国外的Google Play;
- 国内的应用市场非常多,包括360、百度、小米等等;
- 可以根据不同的应用市场相关的规则,上传对应的APK或者AAB文件,填写相关的信息审核即可
- 国外的应用市场通常只有一个Google Play
- 1.需要申请一个Google Play 开发者账号
需要支付25美元注册费用的信用卡,信用卡需要支持Visa, Master Amex, Discover, JCB。
https://play.google.com/apps/publish/signup/ - 2.进入到管理中心,创建应用发布即可
进入了Google Play Console管理中心
- 1.需要申请一个Google Play 开发者账号
iOS打包和发布
-
利用Xcode打开 Flutter工程中的
Runner.xcworkspace
,在Xcode中做相关配置,需要苹果开发者账号,创建AppID,证书,描述文件,然后打包,导出ipa文件,最后可利用transporter
上传ipa文件到苹果公司进行审核; -
注意:如果之前的应用程序是运行在模拟器上的,那么Archive时会报错
-
需要删除ios/Flutter目录下之前生成的App.framework,因为这个framework默认是给模拟器生成的,我们发布的程序要跑在真机设备上;
网友评论