對 Flutter 的興趣空前高漲——而且早就應(yīng)該出現(xiàn)了。 Google 的開源 SDK 與 Android、iOS、macOS、Web、Windows 和 Linux 兼容。單個 Flutter 代碼庫支持所有這些。單元測試有助于交付一致且可靠的 Flutter 應(yīng)用程序,通過在組裝之前先發(fā)制人地提高代碼質(zhì)量來確保不會出現(xiàn)錯誤、缺陷和缺陷。
在本教程中,分享了 Flutter 單元測試的工作流程優(yōu)化,演示了基本的 Flutter 單元測試,然后轉(zhuǎn)向更復(fù)雜的 Flutter 測試用例和庫。
Flutter單元測試的流程
在 Flutter 實現(xiàn)單元測試的方式與在其他技術(shù)棧中的方式大致相同:
1.評估代碼
2.設(shè)置模擬數(shù)據(jù)
3.定義測試組
4.為每個測試組定義測試函數(shù)簽名
5.寫測試用例
為了演示單元測試,我準(zhǔn)備了一個示例 Flutter 項目。該項目使用外部 API 來獲取和顯示可以按國家過濾的大學(xué)列表。
關(guān)于 Flutter 工作原理的一些注意事項: 該框架通過在創(chuàng)建項目時自動加載 flutter_test庫來促進測試。該庫使 Flutter 能夠讀取、運行和分析單元測試。Flutter 還會自動創(chuàng)建用于存儲測試的test文件夾。避免重命名和/或移動test文件夾至關(guān)重要,因為這會破壞其功能,從而破壞運行測試的能力。在測試文件名中包含 _test.dart也很重要,因為這個后綴是 Flutter 識別測試文件的方式。
測試目錄結(jié)構(gòu)
為了在項目中進行單元測試,使用干凈的架構(gòu)實現(xiàn)了 MVVM和依賴注入 (DI) ,正如為源代碼子文件夾選擇的名稱所證明的那樣。MVVM 和 DI 原則的結(jié)合確保了關(guān)注點分離:
1.每個項目類都支持一個目標(biāo)。
2.類中的每個函數(shù)只完成它自己的范圍。
給編寫的測試文件創(chuàng)建一個有組織的存儲空間,在這個系統(tǒng)中,測試組將具有易于識別的“家”。鑒于 Flutter 要求在測試文件夾中定位測試,我們將test目錄下test文件組織成和源碼相同的結(jié)構(gòu)。然后,編寫測試時,將其存儲在適當(dāng)?shù)淖游募A中:就像干凈的襪子放在梳妝臺的襪子抽屜里,折疊的襯衫放在襯衫抽屜里一樣,Model類的單元測試放在名為 model 的文件夾中 , 例如。
項目的測試文件夾結(jié)構(gòu)反映了源代碼結(jié)構(gòu),采用此文件系統(tǒng)可以使項目透明化,并為團隊提供一種簡單的方法來查看代碼的哪些部分具有相關(guān)測試?,F(xiàn)在準(zhǔn)備將單元測試付諸實踐。
一個簡單的 Flutter 單元測試
現(xiàn)在將從model類(在源代碼的data層中)開始,并將示例限制為僅包含一個model類 ApiUniversityModel。此類擁有兩個功能:
●通過使用 Map模擬 JSON 對象來初始化模型。
●構(gòu)建University數(shù)據(jù)模型。
為了測試模型的每個功能,這里自定義一下前面描述的通用步驟:
1.評估代碼
2.設(shè)置數(shù)據(jù)模擬:將定義服務(wù)器對 API 調(diào)用的響應(yīng)
3.定義測試組:將有兩個測試組,每個功能一個
4.為每個測試組定義測試函數(shù)簽名
5.編寫測試用例
評估我們的代碼后,我們準(zhǔn)備實現(xiàn)第二個目標(biāo):設(shè)置特定于ApiUniversityModel類中的兩個函數(shù)的數(shù)據(jù)模擬。
為了模擬第一個函數(shù)(通過使用 Map模擬 JSON 來初始化模型)fromJson,創(chuàng)建兩個 Map 對象來模擬函數(shù)的輸入數(shù)據(jù)。再創(chuàng)建兩個等效的 ApiUniversityModel 對象,以表示具有所提供輸入的函數(shù)的預(yù)期結(jié)果。
為了模擬第二個函數(shù)(構(gòu)建University數(shù)據(jù)模型)toDomain,創(chuàng)建兩個University對象,這是在先前實例化的ApiUniversityModel 對象中運行此函數(shù)后的預(yù)期結(jié)果:
void main() {
Map<String, dynamic> apiUniversityOneAsJson = {
"alpha_two_code": "US",
"domains": ["marywood.edu"],
"country": "United States",
"state-province": null,
"web_pages": ["http://www.marywood.edu"],
"name": "Marywood University"
};
ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel(
alphaCode: "US",
country: "United States",
state: null,
name: "Marywood University",
websites: ["http://www.marywood.edu"],
domains: ["marywood.edu"],
);
University expectedUniversityOne = University(
alphaCode: "US",
country: "United States",
state: "",
name: "Marywood University",
websites: ["http://www.marywood.edu"],
domains: ["marywood.edu"],
);
Map<String, dynamic> apiUniversityTwoAsJson = {
"alpha_two_code": "US",
"domains": ["lindenwood.edu"],
"country": "United States",
"state-province":"MJ",
"web_pages": null,
"name": "Lindenwood University"
};
ApiUniversityModel expectedApiUniversityTwo = ApiUniversityModel(
alphaCode: "US",
country: "United States",
state:"MJ",
name: "Lindenwood University",
websites: null,
domains: ["lindenwood.edu"],
);
University expectedUniversityTwo = University(
alphaCode: "US",
country: "United States",
state: "MJ",
name: "Lindenwood University",
websites: [],
domains: ["lindenwood.edu"],
);
}
接下來,第三個和第四個目標(biāo),將添加描述性語言來定義測試組和測試函數(shù)簽名:
void main() {
// Previous declarations
group("Test ApiUniversityModel initialization from JSON", () {
test('Test using json one', () {});
test('Test using json two', () {});
});
group("Test ApiUniversityModel toDomain", () {
test('Test toDomain using json one', () {});
test('Test toDomain using json two', () {});
});
}
現(xiàn)在定義了兩個測試的簽名來檢查 fromJson 函數(shù),兩個測試來檢查 toDomain函數(shù)。
為了實現(xiàn)第五個目標(biāo)并編寫測試,將使用 flutter_test庫的 expect 方法將函數(shù)的結(jié)果與預(yù)期進行比較:
void main() {
// Previous declarations
group("Test ApiUniversityModel initialization from json", () {
test('Test using json one', () {
expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson),
expectedApiUniversityOne);
});
test('Test using json two', () {
expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson),
expectedApiUniversityTwo);
});
});
group("Test ApiUniversityModel toDomain", () {
test('Test toDomain using json one', () {
expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson).toDomain(),
expectedUniversityOne);
});
test('Test toDomain using json two', () {
expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson).toDomain(),
expectedUniversityTwo);
});
});
}
完成五個目標(biāo)后,現(xiàn)在可以從 IDE 或命令行運行測試。
在終端,可以通過輸入 flutter test 命令來運行test文件夾中包含的所有測試,并查看測試是否通過。或者,可以通過輸入 flutter test --plain-name "ReplaceWithName"命令來運行單個測試或測試組,用測試或測試組的名稱替換 ReplaceWithName。
在 Flutter 中對端點進行單元測試
完成了一個沒有依賴項的簡單測試后,讓我們探索一個更有趣的示例:將測試endpoint類,其范圍包括:
●執(zhí)行對服務(wù)器的 API 調(diào)用。
●將 API JSON 響應(yīng)轉(zhuǎn)換為不同的格式。
在評估了代碼之后,將使用 flutter_test庫的 setUp方法來初始化測試組中的類:
group("Test University Endpoint API calls", () {
setUp(() {
baseUrl = "https://test.url";
dioClient = Dio(BaseOptions());
endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
});
}
要向 API 發(fā)出網(wǎng)絡(luò)請求,更喜歡使用改造庫,它會生成大部分必要的代碼。 為了正確測試 UniversityEndpoint類,將強制 dio 庫(Retrofit 用于執(zhí)行 API 調(diào)用)通過自定義響應(yīng)適配器模擬 Dio 類的行為來返回所需的結(jié)果。
自定義網(wǎng)絡(luò)攔截器
由于通過 DI 構(gòu)建了UniversityEndpoint類,因此可以進行自定義網(wǎng)絡(luò)攔截器。 (如果 UniversityEndpoint 類自己初始化一個 Dio 類,就沒有辦法模擬類的行為。)
為了模擬Dio類的行為,需要知道 Retrofit庫中使用的 Dio方法—— 但無法直接訪問 Dio。 因此,將使用自定義網(wǎng)絡(luò)響應(yīng)攔截器模擬 Dio:
class DioMockResponsesAdapter extends HttpClientAdapter {
final MockAdapterInterceptor interceptor;
DioMockResponsesAdapter(this.interceptor);
@override
void close({bool force = false}) {}
@override
Future<ResponseBody> fetch(RequestOptions options,
Stream<Uint8List>? requestStream, Future? cancelFuture) {
if (options.method == interceptor.type.name.toUpperCase() &&
options.baseUrl == interceptor.uri &&
options.queryParameters.hasSameElementsAs(interceptor.query) &&
options.path == interceptor.path) {
return Future.value(ResponseBody.fromString(
jsonEncode(interceptor.serializableResponse),
interceptor.responseCode,
headers: {
"content-type": ["application/json"]
},
));
}
return Future.value(ResponseBody.fromString(
jsonEncode(
{"error": "Request doesn't match the mock interceptor details!"}),
-1,
statusMessage: "Request doesn't match the mock interceptor details!"));
}
}
enum RequestType { GET, POST, PUT, PATCH, DELETE }
class MockAdapterInterceptor {
final RequestType type;
final String uri;
final String path;
final Map<String, dynamic> query;
final Object serializableResponse;
final int responseCode;
MockAdapterInterceptor(this.type, this.uri, this.path, this.query,
this.serializableResponse, this.responseCode);
}
現(xiàn)在已經(jīng)創(chuàng)建了攔截器來模擬網(wǎng)絡(luò)響應(yīng),接下來可以定義測試組和測試函數(shù)簽名。在例子中,只有一個函數(shù)要測試 (getUniversitiesByCountry),因此將只創(chuàng)建一個測試組。現(xiàn)測試函數(shù)對三種情況的響應(yīng):
1.Dio類的函數(shù)是否真的被 getUniversitiesByCountry 調(diào)用了?
2.如果API 請求返回錯誤,會發(fā)生什么?
3.如果 API 請求返回預(yù)期結(jié)果,會發(fā)生什么?
這是測試組和測試函數(shù)簽名:
group("Test University Endpoint API calls", () {
test('Test endpoint calls dio', () async {});
test('Test endpoint returns error', () async {});
test('Test endpoint calls and returns 2 valid universities', () async {});
});
現(xiàn)在準(zhǔn)備好編寫測試用例了。對于每個測試用例,要創(chuàng)建一個具有相應(yīng)配置的 DioMockResponsesAdapter 實例:
group("Test University Endpoint API calls", () {
setUp(() {
baseUrl = "https://test.url";
dioClient = Dio(BaseOptions());
endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
});
test('Test endpoint calls dio', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
200,
[],
);
var result = await endpoint.getUniversitiesByCountry("us");
expect(result, <ApiUniversityModel>[]);
});
test('Test endpoint returns error', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
404,
{"error": "Not found!"},
);
List<ApiUniversityModel>? response;
DioError? error;
try {
response = await endpoint.getUniversitiesByCountry("us");
} on DioError catch (dioError, _) {
error = dioError;
}
expect(response, null);
expect(error?.error, "Http status error [404]");
});
test('Test endpoint calls and returns 2 valid universities', () async {
dioClient.httpClientAdapter = _createMockAdapterForSearchRequest(
200,
generateTwoValidUniversities(),
);
var result = await endpoint.getUniversitiesByCountry("us");
expect(result, expectedTwoValidUniversities());
});
});
現(xiàn)在端點測試已經(jīng)完成,開始測試數(shù)據(jù)源類 UniversityRemoteDataSource。早些時候,可以看到UniversityEndpoint類是構(gòu)造函數(shù)UniversityRemoteDataSource({UniversityEndpoint? universityEndpoint}) 的一部分,這表明 UniversityRemoteDataSource使用 UniversityEndpoint 類來實現(xiàn)其范圍,因此這是將模擬的類。
使用 Mockito 進行模擬
在之前的示例中,使用自定義 NetworkInterceptor 手動模擬了 Dio 客戶端的請求適配器。手動執(zhí)行此操作(模擬類及其函數(shù))將非常耗時。 幸運的是,模擬庫旨在處理此類情況,并且可以毫不費力地生成模擬類。 使用 mockito 庫,這是 Flutter 中用于模擬的行業(yè)標(biāo)準(zhǔn)庫。為了通過 Mockito 進行模擬,
首先在測試代碼之前添加注釋“@GenerateMocks([class_1,class_2,…])”——就在void main() {}函數(shù)之上。 在注釋中,將包含一個類名列表作為參數(shù)(代替 class_1、class_2…)。
接下來,運行 Flutter 的flutter pub run build_runner構(gòu)建命令,在與測試相同的目錄中為我們的模擬類生成代碼。 生成的模擬文件的名稱將是測試文件名加上.mocks.dart的組合,替換測試的 .dart后綴。
該文件的內(nèi)容將包括名稱以前綴 Mock開頭的模擬類。 例如,UniversityEndpoint 變?yōu)?MockUniversityEndpoint。
現(xiàn)在,將 university_remote_data_source_test.dart.mocks.dart(模擬文件)導(dǎo)入 university_remote_data_source_test.dart(測試文件)。
然后,在 setUp 函數(shù)中,通過使用 MockUniversityEndpoint并初始化 UniversityRemoteDataSource類來模擬 UniversityEndpoint:
import 'university_remote_data_source_test.mocks.dart';
@GenerateMocks([UniversityEndpoint])
void main() {
late UniversityEndpoint endpoint;
late UniversityRemoteDataSource dataSource;
group("Test function calls", () {
setUp(() {
endpoint = MockUniversityEndpoint();
dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
});
}
成功模擬了UniversityEndpoint,然后初始化了UniversityRemoteDataSource 類。 現(xiàn)在準(zhǔn)備好定義測試組和測試函數(shù)簽名:
group("Test function calls", () {
test('Test dataSource calls getUniversitiesByCountry from endpoint', () {});
test('Test dataSource maps getUniversitiesByCountry response to Stream', () {});
test('Test dataSource maps getUniversitiesByCountry response to Stream with error', () {});
});
這樣,模擬、測試組和測試函數(shù)簽名就設(shè)置好了。 已準(zhǔn)備好編寫實際測試。
第一個測試檢查當(dāng)數(shù)據(jù)源啟動國家信息獲取時是否調(diào)用了 UniversityEndpoint 函數(shù)。 首先定義每個類在調(diào)用其函數(shù)時將如何反應(yīng)。 由于模擬了 UniversityEndpoint類,這就是將使用的類,使用 when(function_that_will_be_called).then(what_will_be_returned)代碼結(jié)構(gòu)。
正在測試的函數(shù)是異步的(返回 Future 對象的函數(shù)),因此使用when(function name).thenanswer( () {modified function result} )代碼結(jié)構(gòu)來修改結(jié)果。要檢查 getUniversitiesByCountry 函數(shù)是否調(diào)用了 UniversityEndpoint類中的 getUniversitiesByCountry 函數(shù),使用 when(…).thenAnswer( () {…} )來模擬 UniversityEndpoint 類中的 getUniversitiesByCountry 函數(shù):
when(endpoint.getUniversitiesByCountry("test"))
.thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
現(xiàn)在已經(jīng)模擬了響應(yīng),調(diào)用數(shù)據(jù)源函數(shù)并使用驗證函數(shù)檢查是否調(diào)用了UniversityEndpoint函數(shù):
test('Test dataSource calls getUniversitiesByCountry from endpoint', () {
when(endpoint.getUniversitiesByCountry("test"))
.thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
dataSource.getUniversitiesByCountry("test");
verify(endpoint.getUniversitiesByCountry("test"));
});
可以使用相同的原則來編寫額外的測試來檢查函數(shù)是否正確地將端點結(jié)果轉(zhuǎn)換為相關(guān)的數(shù)據(jù)流:
import 'university_remote_data_source_test.mocks.dart';
@GenerateMocks([UniversityEndpoint])
void main() {
late UniversityEndpoint endpoint;
late UniversityRemoteDataSource dataSource;
group("Test function calls", () {
setUp(() {
endpoint = MockUniversityEndpoint();
dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint);
});
test('Test dataSource calls getUniversitiesByCountry from endpoint', () {
when(endpoint.getUniversitiesByCountry("test"))
.thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
dataSource.getUniversitiesByCountry("test");
verify(endpoint.getUniversitiesByCountry("test"));
});
test('Test dataSource maps getUniversitiesByCountry response to Stream',
() {
when(endpoint.getUniversitiesByCountry("test"))
.thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
expect(
dataSource.getUniversitiesByCountry("test"),
emitsInOrder([
const AppResult<List<University>>.loading(),
const AppResult<List<University>>.data([])
]),
);
});
test(
'Test dataSource maps getUniversitiesByCountry response to Stream with error',
() {
ApiError mockApiError = ApiError(
statusCode: 400,
message: "error",
errors: null,
);
when(endpoint.getUniversitiesByCountry("test"))
.thenAnswer((realInvocation) => Future.error(mockApiError));
expect(
dataSource.getUniversitiesByCountry("test"),
emitsInOrder([
const AppResult<List<University>>.loading(),
AppResult<List<University>>.apiError(mockApiError)
]),
);
});
});
}
我們已經(jīng)執(zhí)行了許多 Flutter 單元測試并演示了不同的模擬方法。 可以繼續(xù)使用示例Flutter 項目來運行其他測試。
Flutter 單元測試:實現(xiàn)卓越用戶體驗的關(guān)鍵
如果已經(jīng)將單元測試整合到 Flutter 項目中,本文可能已經(jīng)介紹了一些可以注入到工作流程中的新選項。 在本教程中,演示了將單元測試合并到下一個 Flutter 項目中是多么簡單,以及如何應(yīng)對更細(xì)微的測試場景的挑戰(zhàn)。你可能再也不想跳過 Flutter 中的單元測試了。
最后感謝每一個認(rèn)真閱讀我文章的人,禮尚往來總是要有的,雖然不是什么很值錢的東西,如果你用得到的話可以直接拿走:文章來源:http://www.zghlxwxcb.cn/news/detail-712549.html
這些資料,對于【軟件測試】的朋友來說應(yīng)該是最全面最完整的備戰(zhàn)倉庫,這個倉庫也陪伴上萬個測試工程師們走過最艱難的路程,希望也能幫助到你!???文章來源地址http://www.zghlxwxcb.cn/news/detail-712549.html
到了這里,關(guān)于Flutter 中的單元測試:從工作流基礎(chǔ)到復(fù)雜場景的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!