作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
对颤振的兴趣正在下降 历史新高而且早就该这么做了. 谷歌的开源SDK兼容 安卓、iOS、macOS、web、Windows等 Linux. 一个颤振代码库就可以支持所有这些功能. 单元测试在交付一致和可靠的产品方面很有帮助 颤振 App,通过先发制人的改进来防止错误、缺陷和缺陷 代码质量 在组装之前.
在这个颤振测试教程中, 我们分享了颤振单元测试的工作流程优化, 演示一个基本的颤振单元测试示例, 然后转到更复杂的颤振测试用例和库.
我们在颤振中实现单元测试的方式与在其他技术栈中实现单元测试的方式大致相同:
为了演示如何进行颤振测试,我准备了一个 样本颤振项目 鼓励大家使用And 测试代码 有空的时候. 该项目使用 外部API 获取并显示我们可以按国家过滤的大学列表.
关于颤振如何工作的一些注意事项:该框架通过自动加载 flutter_t
图书馆 创建项目时. 该库使颤振能够读取、运行和分析单元测试. 颤振也会自动创建 测试
用于存储测试的文件夹. 方法的重命名和/或移动是至关重要的 测试
文件夹,因为这会破坏它的功能,从而破坏我们运行测试的能力. 它也必须包含 _t.飞镖
在我们的测试文件名中,因为这个后缀是颤振识别测试文件的方式.
为了促进项目中的单元测试,我们实现了 具有干净架构的MVVM 和 依赖注入(DI),如为源代码子文件夹选择的名称所示. 的结合 MVVM 和DI原则确保了关注点的分离:
我们将为将要编写的测试文件创建一个有组织的存储空间, 在这个系统中,一组测试将有容易识别的“家”."鉴于颤振要求将测试定位在 测试
文件夹,让我们在下面镜像源代码的文件夹结构 测试
. 然后, 当我们编写测试时, 我们会把它放在合适的子文件夹里:就像干净的袜子放在梳妆台的袜子抽屉里,叠好的衬衫放在衬衫抽屉里一样, 的单元测试 模型
类放在名为 模型
例如:.
采用这个文件系统可以在项目中建立透明度,并为团队提供一种查看代码的哪些部分具有相关测试的简单方法.
现在我们准备将单元测试付诸行动.
我们从 模型
类(在 data
层),并将我们的示例限制为只包含一个 模型
class, Api大学模型
. 这个类有两个功能:
Map
.大学
数据模型.为了测试模型的每个功能,我们将定制前面描述的通用步骤:
在评估了我们的代码之后, 我们已经准备好完成第二个目标:设置特定于的两个函数的数据模拟 Api大学模型
class.
来模拟第一个函数(通过模拟JSON来初始化模型) Map
), fromJson
,我们将创建两个 Map
对象来模拟函数的输入数据. 我们也会创建两个等价的 Api大学模型
对象来表示使用所提供的输入的函数的预期结果.
要模拟第二个函数(构建 大学
数据模型), toDomain
,我们将创建两个 大学
对象,这是在先前实例化的对象中运行此函数后的预期结果 Api大学模型
对象:
Void main() {
Map api大学OneAsJson = {
“alpha_two_code”:“我们”,
“域”:[" marywood.edu”),
“国家”:“美国”;
“状态省”:空,
“web_pages”:[" http://www.marywood.edu”),
"名字": "Marywood 大学"
};
Api大学模型 预计edApi大学One = Api大学模型(
alphaCode:“我们”,
国家:“美国”;
状态:空,
校名:“玛丽伍德大学”
[" http://www网站:.marywood.edu”),
域:[" marywood.edu”),
);
期望就读的大学
alphaCode:“我们”,
国家:“美国”;
状态:“”,
校名:“玛丽伍德大学”
[" http://www网站:.marywood.edu”),
域:[" marywood.edu”),
);
Map api大学TwoAsJson = {
“alpha_two_code”:“我们”,
“域”:[" lindenwood.edu”),
“国家”:“美国”;
“状态省”:“乔丹”,
“web_pages”:空,
"名字": "Lindenwood 大学"
};
Api大学模型 预计edApi大学Two = Api大学模型(
alphaCode:“我们”,
国家:“美国”;
状态:“乔丹”,
校名:林登伍德大学
网站:空,
域:[" lindenwood.edu”),
);
期望就读的大学
alphaCode:“我们”,
国家:“美国”;
状态:“乔丹”,
校名:林登伍德大学
网站:[],
域:[" lindenwood.edu”),
);
}
下一个, 我们的第三和第四个目标, 我们将添加描述性语言来定义我们的测试组和测试函数签名:
Void main() {
//之前的声明
group("Test Api大学模型 initialization from JSON", () {
测试(' 测试 using json one', () {});
测试(' 测试 using json two', () {});
});
group("Test Api大学模型 toDomain", () {
测试(' 测试 toDomain using json one', () {});
测试(' 测试 toDomain using json two', () {});
});
}
我们定义了两个测试的签名来检查 fromJson
函数,以及两个检查 toDomain
函数.
要实现第五个目标并编写测试,让我们使用 flutter_t图书馆’s 预计
将函数结果与我们的期望进行比较的方法:
Void main() {
//之前的声明
group("Test Api大学模型 initialization from json", () {
测试(' 测试 using json one', () {
期望(Api大学模型.fromJson (api大学OneAsJson),
预计edApi大学One);
});
测试(' 测试 using json two', () {
期望(Api大学模型.fromJson (api大学TwoAsJson),
预计edApi大学Two);
});
});
group("Test Api大学模型 toDomain", () {
测试(' 测试 toDomain using json one', () {
期望(Api大学模型.fromJson (api大学OneAsJson).toDomain (),
预计ed大学One);
});
测试(' 测试 toDomain using json two', () {
期望(Api大学模型.fromJson (api大学TwoAsJson).toDomain (),
预计ed大学Two);
});
});
}
完成了五个目标之后,我们现在可以从IDE或从 命令行.
中包含的所有测试都可以在终端上运行 测试
文件夹中输入 颤振试验
命令,确保我们的测试通过.
或者,我们可以通过输入。来运行单个测试或测试组 颤振试验—纯名称“ReplaceWithName”
命令,将我们的测试或测试组的名称替换为 ReplaceWithName
.
完成了一个没有依赖项的简单测试, 让我们探索一个更有趣的颤振单元测试示例:我们将测试 端点
class,其范围包括:
在对代码求值之后,我们将使用 flutter_t图书馆’s 设置
方法初始化测试组中的类:
group(“测试大学端点API调用”,(){
设置((){
baseUrl = "http://测试.url”;
dioClient = 戴奥(BaseOptions());
端点 = 大学Endpoint(dioClient, baseUrl: baseUrl);
});
}
要向api发出网络请求,我更喜欢使用 改造图书馆,它生成大部分必需的代码. 要正确测试 大学Endpoint
同学们,我们将强制 戴奥库——这 改造
方法来执行API调用,从而返回所需的结果 戴奥
类的行为通过自定义响应适配器.
嘲笑是可能的,因为我们已经建立了 大学Endpoint
直通DI类. (如果 大学Endpoint
类初始化 戴奥
类本身,我们将无法模拟类的行为.)
为了嘲弄 戴奥
类的行为,我们需要知道 戴奥
方法中使用的 改造
图书馆——但我们不能直接进入 戴奥
. 因此,我们将嘲笑 戴奥
使用自定义网络响应拦截器:
类戴奥模拟ResponsesAdapter扩展HttpClientAdapter {
最后的模拟AdapterInterceptor拦截器;
戴奥模拟ResponsesAdapter(这.拦截器);
@override
无效关闭({bool force = false}) {}
@override
未来 fetch(RequestOptions 选项,
Stream? requestStream,未来? cancel未来) {
如果选项.方法==拦截器.类型.名字.toUpperCase () &&
选项.baseUrl ==拦截器.uri &&
选项.queryParameters.hasSameElementsAs(拦截器.查询) &&
选项.路径==拦截器.路径){
回到未来.值(ResponseBody.fromString (
jsonEncode (拦截器.serializableResponse),
拦截器.responseCode,
标题:{
“内容类型”:[" application / json ")
},
));
}
回到未来.值(ResponseBody.fromString (
jsonEncode (
{"error": "请求与模拟拦截器细节不匹配!"}),
-1,
statusMessage: "请求与模拟拦截器细节不匹配!"));
}
}
enum请求类型{GET、POST、PUT、PATCH、DELETE}
模拟AdapterInterceptor {
最后的RequestType类型;
最终字符串uri;
最后的字符串路径;
final Map query;
最终对象serializable响应;
int responseCode;
模拟AdapterInterceptor(这.类型,这.uri,这.道路,这.查询,
这.serializableResponse,这.responseCode);
}
现在我们已经创建了拦截器来模拟我们的网络响应, 我们可以定义我们的测试组和测试函数签名.
在本例中,我们只有一个函数要测试(getUniversitiesByCountry
),所以我们只创建一个测试组. 我们将测试函数对三种情况的响应:
戴奥
类实际调用的函数 getUniversitiesByCountry
?下面是我们的测试组和测试函数签名:
group(“测试大学端点API调用”,(){
测试('测试端点调用dio', () async {});
测试('测试端点返回错误',()async {});
测试('测试端点调用并返回2个有效的大学',()async {});
});
我们已经准备好编写测试了. 对于每个测试用例,我们将创建的实例 戴奥模拟ResponsesAdapter
具有相应的配置:
group(“测试大学端点API调用”,(){
设置((){
baseUrl = "http://测试.url”;
dioClient = 戴奥(BaseOptions());
端点 = 大学Endpoint(dioClient, baseUrl: baseUrl);
});
测试('测试端点调用dio', () async {
dioClient.httpClientAdapter = _create模拟AdapterFor搜索Request(
200,
[],
);
Var结果=等待端点.getUniversitiesByCountry(“我们”);
期望(result, []);
});
测试('测试端点返回错误',()async {
dioClient.httpClientAdapter = _create模拟AdapterFor搜索Request(
404,
{"error": "Not found .!"},
);
List? 反应;
戴奥Error? 错误;
尝试{
响应=等待端点.getUniversitiesByCountry(“我们”);
} on 戴奥Error catch (戴奥Error, _) {
error = dioError;
}
期望(响应,null);
期望(错误?.error, "Http状态错误[404]");
});
测试('测试端点调用并返回2个有效大学',()async {
dioClient.httpClientAdapter = _create模拟AdapterFor搜索Request(
200,
generateTwoValidUniversities (),
);
Var结果=等待端点.getUniversitiesByCountry(“我们”);
期望(因此,预计edTwoValidUniversities ());
});
});
现在端点测试已经完成,让我们测试数据源类, 大学RemoteDataSource
. 早些时候,我们观察到 大学Endpoint
类是构造函数的一部分 大学RemoteDataSource ({大学Endpoint? universityEndpoint})
,这表明 大学RemoteDataSource
使用 大学Endpoint
类来完成它的作用域,所以这就是我们要模拟的类.
在前面的示例中,我们手动模拟了 戴奥
使用自定义的客户端请求适配器 NetworkInterceptor
. 我们这是在嘲笑全班同学. 手动模拟类及其函数会非常耗时. 幸运的是, 模拟库就是为处理这种情况而设计的,并且可以以最小的工作量生成模拟类. 让我们使用 5图书馆在颤振中用于mock的行业标准库.
嘲笑 5
,我们首先添加注释@Generate模拟s ([class_1, class_2,…)
,在测试代码之前——就在 Void main() {}
函数. 在注释中,我们将包含一个类名列表作为参数(代替 class_1 class_2…
).
接下来,我们运行颤振 's 颤振 pub运行build_runner构建
命令,该命令在与测试相同的目录中为模拟类生成代码. 生成的模拟文件名将是测试文件名加上的组合 .模拟.飞镖
,替换测试 .飞镖
后缀. 文件的内容将包括名称以前缀开头的模拟类 模拟
. 例如, 大学Endpoint
就变成了 模拟大学Endpoint
.
现在,我们导入 university_remote_data_source_t.飞镖.模拟.飞镖
(我们的模拟文件)放入 university_remote_data_source_t.飞镖
(测试文件).
然后,在 设置
函数,我们将进行模拟 大学Endpoint
通过使用 模拟大学Endpoint
初始化 大学RemoteDataSource
类:
进口的university_remote_data_source_t.模拟.飞镖”;
@Generate模拟s ([大学Endpoint])
Void main() {
晚期大学Endpoint端点;
remotedatasource
group(“测试函数调用”,(){
设置((){
端点 = 模拟大学Endpoint();
数据源 = 大学RemoteDataSource(universityEndpoint: 端点);
});
}
我们成功地 大学Endpoint
然后初始化 大学RemoteDataSource
class. 现在我们可以定义测试组和测试函数签名了:
group(“测试函数调用”,(){
测试('测试数据源从端点调用getUniversitiesByCountry ', () {});
测试('测试数据源映射getUniversitiesByCountry响应到流',(){});
测试('测试数据源映射getUniversitiesByCountry响应到有错误的流',(){});
});
这样,我们的模拟、测试组和测试函数签名就设置好了. 我们已经准备好编写实际的测试了.
我们的第一个测试检查 大学Endpoint
函数在数据源初始化获取国家/地区信息时调用. 我们首先定义每个类在调用其函数时的反应. 既然我们嘲笑 大学Endpoint
类,这是我们要用到的类,使用 当(函数_that_will_be_called).然后(what_will_be_returned)
代码结构.
我们正在测试的函数是异步的(返回a 未来
对象),所以我们将使用 当(函数名).然后回答((_){修改后的函数结果})
代码结构来修改我们的结果.
要检查 getUniversitiesByCountry
函数调用 getUniversitiesByCountry
内部的功能 大学Endpoint
类,我们将使用 当(...).然后回答((_){...} )
嘲笑 getUniversitiesByCountry
内部的功能 大学Endpoint
类:
当(端点.getUniversitiesByCountry(“测试”))
.thenAnswer((realInvocation) => 未来.value([]));
现在我们已经模拟了响应,我们调用数据源函数并检查——使用 验证
函数-whether的 大学Endpoint
函数被调用:
测试('测试数据源从端点调用getUniversitiesByCountry ', () {
当(端点.getUniversitiesByCountry(“测试”))
.thenAnswer((realInvocation) => 未来.value([]));
数据源.getUniversitiesByCountry(“测试”);
验证(端点.getUniversitiesByCountry(“测试”));
});
我们可以使用相同的原则编写额外的测试,检查函数是否正确地将端点结果转换为相关的数据流:
进口的university_remote_data_source_t.模拟.飞镖”;
@Generate模拟s ([大学Endpoint])
Void main() {
晚期大学Endpoint端点;
remotedatasource
group(“测试函数调用”,(){
设置((){
端点 = 模拟大学Endpoint();
数据源 = 大学RemoteDataSource(universityEndpoint: 端点);
});
测试('测试数据源从端点调用getUniversitiesByCountry ', () {
当(端点.getUniversitiesByCountry(“测试”))
.thenAnswer((realInvocation) => 未来.value([]));
数据源.getUniversitiesByCountry(“测试”);
验证(端点.getUniversitiesByCountry(“测试”));
});
测试('测试数据源映射getUniversitiesByCountry对流的响应',
() {
当(端点.getUniversitiesByCountry(“测试”))
.thenAnswer((realInvocation) => 未来.value([]));
期望(
数据源.getUniversitiesByCountry(“测试”),
emitsInOrder ([
const AppResult>.加载(),
const AppResult>.数据([])
]),
);
});
测试(
'测试数据源映射getUniversitiesByCountry响应到错误的流',
() {
mockApiError = ApiError(
statusCode: 400,
消息:“错误”,
错误:空,
);
当(端点.getUniversitiesByCountry(“测试”))
.thenAnswer((realInvocation) => 未来.错误(mockApiError));
期望(
数据源.getUniversitiesByCountry(“测试”),
emitsInOrder ([
const AppResult>.加载(),
AppResult>.apiError (mockApiError)
]),
);
});
});
}
我们已经执行了大量的颤振单元测试,并演示了不同的模拟方法. 我邀请你继续使用我的 样本颤振项目 运行额外的测试.
如果您已经将单元测试合并到您的颤振项目中, 本文可能介绍了一些您可以注入到工作流中的新选项. 在本教程中, 我们演示了在您的软件中使用单元测试最佳实践是多么简单 下一个颤振项目 以及如何应对更微妙的测试场景的挑战. 您可能再也不想跳过颤振中的单元测试了.
Toptal工程博客的编辑团队向 Matija是čirević 感谢Paul Hoskins审阅了本文中的代码示例和其他技术内容.
在颤振中进行单元测试的过程与在大多数框架中一样. 在定义了要测试的类和函数(测试用例)之后, (1)对规范进行评估, (2)建立数据模拟, (3)确定测试组, (4)定义每个测试组的测试功能签名, (5)编写并运行测试.
单元测试可以防止或大大减少应用程序中的错误, 在应用首次发布时就提供高质量的用户体验. 一个额外的好处是:阅读单元测试可以帮助新开发人员学习和理解代码.
MVVM(模型-视图-视图模型模式)增强了代码库的稳定性和可伸缩性. 代码增强是我们编写更干净的代码以符合MVVM的架构需求的自然结果.
MVVM架构模块化了我们的代码:模型模块中的类提供我们的数据. 视图模块通过UI小部件呈现数据. 最后, 视图模型
类获取数据并向其关联的类提供数据 视图
类.
单元测试是测试单个代码片段的过程, 通常是非常小的代码单元,比如类, 方法, 和功能.
世界级的文章,每周发一次.
世界级的文章,每周发一次.