單元測試驗(yàn)證單個(gè)方法或類是否按預(yù)期工作。它還通過在進(jìn)行新更改時(shí)確認(rèn)現(xiàn)有邏輯是否仍然有效來提高可維護(hù)性。
通常,單元測試很容易編寫,但可以在測試環(huán)境中運(yùn)行。400默認(rèn)情況下,這會(huì)在進(jìn)行網(wǎng)絡(luò)調(diào)用或 HTTP 請(qǐng)求時(shí)產(chǎn)生帶有狀態(tài)代碼的空響應(yīng)。為了解決這個(gè)問題,我們可以在每次發(fā)出 HTTP 請(qǐng)求時(shí)輕松使用 Mockito 返回虛假響應(yīng)。Mockito 有各種用例,隨著我們的進(jìn)行,我們將逐步介紹這些用例。
在本教程中,我們將演示如何使用 Mockito 來測試 Flutter 代碼。我們將學(xué)習(xí)如何生成模擬、存根數(shù)據(jù)以及對(duì)發(fā)出流的方法執(zhí)行測試。讓我們開始吧!
-
什么是 Mockito?
-
生成模擬和存根數(shù)據(jù)
-
項(xiàng)目結(jié)構(gòu)概述
-
依賴注入
-
-
使用參數(shù)匹配器
-
在 Mockito 中創(chuàng)建假貨
-
在 Flutter 中模擬和測試流
什么是 Mockito?
Mockito 是一個(gè)眾所周知的包,它可以更輕松地生成現(xiàn)有類的假實(shí)現(xiàn)。它消除了重復(fù)編寫這些功能的壓力。此外,Mockito 有助于控制輸入,因此我們可以測試預(yù)期的結(jié)果。
假設(shè)使用 Mockito 可以更輕松地編寫單元測試,但是,如果架構(gòu)不好,模擬和編寫單元測試很容易變得復(fù)雜。
在本教程的后面,我們將學(xué)習(xí)如何將 Mockito 與模型-視圖-視圖模型 (MVVM) 模式一起使用,該模式涉及將代碼庫分成不同的可測試部分,例如視圖模型和存儲(chǔ)庫。
生成模擬和存根數(shù)據(jù)
模擬是真實(shí)類的假實(shí)現(xiàn)。它們通常用于控制測試的預(yù)期結(jié)果,或者當(dāng)真實(shí)類在測試環(huán)境中容易出錯(cuò)時(shí)。
為了更好地理解這一點(diǎn),我們將為處理發(fā)送和接收帖子的應(yīng)用程序編寫單元測試。
項(xiàng)目結(jié)構(gòu)概述
在開始之前,讓我們將所有必要的包添加到我們的項(xiàng)目中。
dependencies: dio: ^4.0.6 # For making HTTP requests ? dev_dependencies: build_runner: ^2.2.0 # For generating code (Mocks, etc) mockito: ^5.2.0 # For mocking and stubbing
我們將使用 MVVM 和存儲(chǔ)庫模式,其中包括對(duì)存儲(chǔ)庫和視圖模型的測試。在 Flutter 中,將所有測試放在test文件夾中是一個(gè)很好的做法,它與文件夾的結(jié)構(gòu)非常匹配lib。
接下來,我們將通過附加到文件名來創(chuàng)建authentication_repository.dart和文件。這有助于測試運(yùn)行者找到項(xiàng)目中存在的所有測試。authentication_repository_test.dart``_test
超過 20 萬開發(fā)人員使用 LogRocket 來創(chuàng)造更好的數(shù)字體驗(yàn)了解更多 →
我們將通過創(chuàng)建一個(gè)名為AuthRepository. 顧名思義,這個(gè)類將處理我們應(yīng)用程序中的所有身份驗(yàn)證功能。之后,我們將包含一個(gè)登錄方法,該方法檢查狀態(tài)代碼是否相等200并捕獲身份驗(yàn)證時(shí)發(fā)生的任何錯(cuò)誤。
class AuthRepository { ?Dio dio = Dio(); ? ?AuthRepository(); ? ?Future<bool> login({ ? ?required String email, ? ?required String password, }) async { ? ?try { ? ? ?final result = await dio.post( ? ? ? ?'<https://reqres.in/api/login>', ? ? ? ?data: {'email': email, 'password': password}, ? ? ); ? ? ? ?if (result.statusCode != 200) { ? ? ? ?return false; ? ? } ? } on DioError catch (e) { ? ? ?print(e.message); ? ? ?return false; ? } ? ? ?return true; } ? ?// ... } void main() { ?late AuthRepository authRepository; ? ?setUp(() { ? ?authRepository = AuthRepository(); }); ? ?test('Successfully logged in user', () async { ? ?expect( ? ? ?await authRepository.login(email: 'james@mail.com', password: '123456'), ? ? ?true, ? ); }); }
在上面的測試中,我們AuthRepository在 setup 函數(shù)中初始化了 。由于它將在每個(gè)測試和測試組之前直接在內(nèi)部運(yùn)行main,因此它將auth為每個(gè)測試或組初始化一個(gè)新的存儲(chǔ)庫。
接下來,我們將編寫一個(gè)測試,期望登錄方法返回true而不會(huì)拋出錯(cuò)誤。但是,測試仍然失敗,因?yàn)閱卧獪y試默認(rèn)不支持發(fā)出網(wǎng)絡(luò)請(qǐng)求,因此發(fā)出的登錄請(qǐng)求Dio返回狀態(tài)碼400。
為了解決這個(gè)問題,我們可以使用 Mockito 生成一個(gè)模擬類,其功能類似于Dio. 在 Mockito 中,我們通過@GenerateMocks([classes])在方法的開頭添加注釋來生成模擬main。這會(huì)通知構(gòu)建運(yùn)行器為列表中的所有類生成模擬。
@GenerateMocks([Dio, OtherClass]) void main(){ ? ?// test for login }
接下來,打開終端并運(yùn)行命令flutter pub run build_runner build以開始為類生成模擬。代碼生成完成后,我們將能夠通過添加Mock類名來訪問生成的模擬。
@GenerateMocks([Dio]) void main(){ ? ? ?MockDio mockDio = MockDio() ? ? ?late AuthRepository authRepository; ? ? ... }
我們必須對(duì)數(shù)據(jù)進(jìn)行存根,以確保MockDio在調(diào)用登錄端點(diǎn)時(shí)返回正確的響應(yīng)數(shù)據(jù)。在 Flutter 中,存根意味著在調(diào)用 mock 方法時(shí)返回一個(gè)假對(duì)象。例如,當(dāng)測試使用 調(diào)用登錄端點(diǎn)時(shí)MockDio,我們應(yīng)該返回一個(gè)帶有狀態(tài)碼的響應(yīng)對(duì)象200。
可以使用 function 來對(duì) mock 進(jìn)行存根,該 functionwhen()可以與thenReturn,一起使用thenAnswer,或者thenThrow在我們調(diào)用 mock 方法時(shí)提供所需的值。該thenAnswer函數(shù)用于返回未來或流的方法,而thenReturn用于模擬類的普通同步方法。
// To stub any method; gives error when used for futures or stream when(mock.method()).thenReturn(value); ? // To stub method that return a future or stream when(mock.method()).thenAnswer(() => futureOrStream); ? // To stub error when(mock.method()).thenThrow(errorObject); ? // dart @GenerateMocks([Dio]) void main() { ?MockDio mockDio = MockDio(); ?late AuthRepository authRepository; ? ?setUp(() { ? ?authRepository = AuthRepository(); }); ? ?test('Successfully logged in user', () async { ? ?// Stubbing ? ?when(mockDio.post( ? ? ?'<https://reqres.in/api/login>', ? ? ?data: {'email': 'james@mail.com', 'password': '123456'}, ? )).thenAnswer( ? ? (inv) => Future.value(Response( ? ? ? ?statusCode: 200, ? ? ? ?data: {'token': 'ASjwweiBE'}, ? ? ? ?requestOptions: RequestOptions(path: '<https://reqres.in/api/login>'), ? ? )), ? ); ? ? ?expect( ? ? ?await authRepository.login(email: 'james@mail.com', password: '123456'), ? ? ?true, ? ); }); }
創(chuàng)建存根后,我們?nèi)匀恍枰獋魅隡ockDio測試文件,以便使用它而不是真正的dio類。為了實(shí)現(xiàn)這一點(diǎn),我們將從中刪除真實(shí)dio類的定義或?qū)嵗?,authRepository并允許它通過其構(gòu)造函數(shù)傳遞。這個(gè)概念稱為依賴注入。
依賴注入
Flutter 中的依賴注入是一種技術(shù),其中一個(gè)對(duì)象或類提供另一個(gè)對(duì)象的依賴項(xiàng)。這種模式確保測試模型和視圖模型都可以定義dio它們想要使用的類型。
class AuthenticationRepository{ ? ? ? ?Dio dio; ? ? ? ? ?// Instead of specifying the type of dio to be used ? ? ? ?// we let the test or viewmodel define it ? ? ? ?AuthenticationRepository(this.dio) } @GenerateMocks([Dio]) void main() { ?MockDio mockDio = MockDio(); ?late AuthRepository authRepository; ? ?setUp(() { ? ?// we can now pass in Dio as an argument ? ?authRepository = AuthRepository(mockDio); }); }
使用參數(shù)匹配器
在前面的登錄示例中,如果在發(fā)出請(qǐng)求時(shí)james@mail.com更改了電子郵件sam@mail.com,則測試將產(chǎn)生no stub found錯(cuò)誤。這是因?yàn)槲覀冎粸閖ames@mail.com.
但是,在大多數(shù)情況下,我們希望通過使用 Mockito 提供的參數(shù)匹配器來避免重復(fù)不必要的邏輯。使用參數(shù)匹配器,我們可以將相同的存根用于廣泛的值而不是確切的類型。
為了更好地理解匹配參數(shù),我們將測試PostViewModel并為PostRepository. 建議使用這種方法,因?yàn)楫?dāng)我們存根時(shí),我們將返回自定義對(duì)象或模型,而不是響應(yīng)和映射。這也很容易!
首先,我們將創(chuàng)建PostModel更清晰地表示數(shù)據(jù)的 。
class PostModel { ?PostModel({ ? ?required this.id, ? ?required this.userId, ? ?required this.body, ? ?required this.title, }); ? ?final int id; ?final String userId; ?final String body; ?final String title; ? ?// implement fromJson and toJson methods for this }
接下來,我們創(chuàng)建PostViewModel. 這用于檢索或發(fā)送數(shù)據(jù)到PostRepository. PostViewModel只是從存儲(chǔ)庫中發(fā)送和檢索數(shù)據(jù)并通知 UI 使用新數(shù)據(jù)重建。
import 'package:flutter/material.dart'; import 'package:mockito_article/models/post_model.dart'; import 'package:mockito_article/repositories/post_repository.dart'; ? class PostViewModel extends ChangeNotifier { ?PostRepository postRepository; ?bool isLoading = false; ? ?final Map<int, PostModel> postMap = {}; ? ?PostViewModel(this.postRepository); ? ?Future<void> sharePost({ ? ?required int userId, ? ?required String title, ? ?required String body, }) async { ? ?isLoading = true; ? ?await postRepository.sharePost( ? ? ?userId: userId, ? ? ?title: title, ? ? ?body: body, ? ); ? ? ?isLoading = false; ? ?notifyListeners(); } ? ?Future<void> updatePost({ ? ?required int userId, ? ?required int postId, ? ?required String body, }) async { ? ?isLoading = true; ? ?await postRepository.updatePost(postId, body); ? ? ?isLoading = false; ? ?notifyListeners(); } ? ?Future<void> deletePost(int id) async { ? ?isLoading = true; ? ?await postRepository.deletePost(id); ? ? ?isLoading = false; ? ?notifyListeners(); } ? ?Future<void> getAllPosts() async { ? ?isLoading = true; ? ?final postList = await postRepository.getAllPosts(); ? ? ?for (var post in postList) { ? ? ?postMap[post.id] = post; ? } ? ? ?isLoading = false; ? ?notifyListeners(); } }
如前所述,我們模擬依賴關(guān)系而不是我們測試的實(shí)際類。在這個(gè)例子中,我們?yōu)?. 編寫單元測試PostViewModel并模擬PostRepository. 這意味著我們將調(diào)用生成MockPostRepository類中的方法,而不是PostRepository可能引發(fā)錯(cuò)誤的方法。
Mockito 使匹配參數(shù)變得非常容易。例如,看updatePost一下PostViewModel. 它調(diào)用存儲(chǔ)庫updatePost方法,該方法只接受兩個(gè)位置參數(shù)。對(duì)于這個(gè)類方法的存根,我們可以選擇提供精確的postIdand body,或者我們可以使用anyMockito 提供的變量來保持簡單。
@GenerateMocks([PostRepository]) void main() { MockPostRepository mockPostRepository = MockPostRepository(); late PostViewModel postViewModel; setUp(() { postViewModel = PostViewModel(mockPostRepository); }); test('Updated post successfully', () { // stubbing with argument matchers and 'any' when( mockPostRepository.updatePost(any, argThat(contains('stub'))), ).thenAnswer( (inv) => Future.value(), ); // This method calls the mockPostRepository update method postViewModel.updatePost( userId: 1, postId: 3, body: 'include `stub` to receive the stub', ); // verify the mock repository was called verify(mockPostRepository.updatePost(3, 'include `stub` to receive the stub')); }); }
上面的存根包括any變量和argThat(matcher)函數(shù)。在 Dart 中,匹配器用于指定測試期望。我們有不同類型的匹配器適用于不同的測試用例。例如,如果對(duì)象包含相應(yīng)的值,則匹配器contains(value)返回。true
匹配位置參數(shù)和命名參數(shù)
在 Dart 中,我們也有位置參數(shù)和命名參數(shù)。在上面的示例中,方法的模擬和存根updatePost處理位置參數(shù)并使用any變量。
但是,命名參數(shù)不支持any變量,因?yàn)?Dart 沒有提供一種機(jī)制來知道元素是否用作命名參數(shù)。相反,我們anyNamed(’name’)在處理命名參數(shù)時(shí)使用該函數(shù)。
when( mockPostRepository.sharePost( body: argThat(startsWith('stub'), named: 'body'), postId: anyNamed('postId'), title: anyNamed('title'), userId: 3, ), ).thenAnswer( (inv) => Future.value(), );
當(dāng)使用帶有命名參數(shù)的匹配器時(shí),我們必須提供參數(shù)的名稱以避免錯(cuò)誤。您可以在 Dart 文檔中閱讀有關(guān)匹配器的更多信息,以查看所有可能的可用選項(xiàng)。
向日葵遠(yuǎn)程控制軟件,居家辦公必備神器,支持手機(jī)控制電腦遠(yuǎn)程傳輸文件!
在 Mockito 中創(chuàng)建假貨
模擬和假貨經(jīng)常被混淆,所以讓我們快速澄清兩者之間的區(qū)別。
模擬是生成的類,允許使用參數(shù)匹配器進(jìn)行存根。然而,F(xiàn)ake 是覆蓋真實(shí)類的現(xiàn)有方法以提供更大靈活性的類,所有這些都無需使用參數(shù)匹配器。
例如,在 post 存儲(chǔ)庫中使用 fakes 而不是 mocks 將允許我們使 fake repository 功能類似于真實(shí)存儲(chǔ)庫。這是可能的,因?yàn)槲覀兡軌蚋鶕?jù)提供的值返回結(jié)果。簡單來說,當(dāng)我們調(diào)用sharePost測試時(shí),我們可以選擇保存帖子,稍后再確認(rèn)帖子是否被保存getAllPosts。
class FakePostRepository extends Fake implements PostRepository { Map<int, PostModel> fakePostStore = {}; @override Future<PostModel> sharePost({ int? postId, required int userId, required String title, required String body, }) async { final post = PostModel( id: postId ?? 0, userId: userId, body: body, title: title, ); fakePostStore[postId ?? 0] = post; return post; } @override Future<void> updatePost(int postId, String body) async { fakePostStore[postId] = fakePostStore[postId]!.copyWith(body: body); } @override Future<List<PostModel>> getAllPosts() async { return fakePostStore.values.toList(); } @override Future<bool> deletePost(int id) async { fakePostStore.remove(id); return true; } }
更新后的測試使用fake如下所示。使用fake,我們可以一次測試所有方法。帖子在添加或共享時(shí)將獲取到存儲(chǔ)庫中的地圖。
@GenerateMocks([PostRepository]) void main() { FakePostRepository fakePostRepository = FakePostRepository(); late PostViewModel postViewModel; setUp(() { postViewModel = PostViewModel(fakePostRepository); }); test('Updated post successfully', () async { expect(postViewModel.postMap.isEmpty, true); const postId = 123; postViewModel.sharePost( postId: postId, userId: 1, title: 'First Post', body: 'My first post', ); await postViewModel.getAllPosts(); expect(postViewModel.postMap[postId]?.body, 'My first post'); postViewModel.updatePost( postId: postId, userId: 1, body: 'My updated post', ); await postViewModel.getAllPosts(); expect(postViewModel.postMap[postId]?.body, 'My updated post'); }); }
在 Flutter 中模擬和測試流
使用 Mockito 模擬和存根流與期貨非常相似,因?yàn)槲覀儗?duì)存根使用相同的語法。然而,流與期貨有很大不同,因?yàn)樗鼈兲峁┝艘环N機(jī)制,可以在發(fā)出值時(shí)持續(xù)監(jiān)聽它們。
要測試返回流的方法,我們可以測試該方法是否被調(diào)用或檢查值是否以正確的順序發(fā)出。
class PostViewModel extends ChangeNotifier { ... ?PostRepository postRepository; ?final likesStreamController = StreamController<int>(); ? ?PostViewModel(this.postRepository); ? ... ?void listenForLikes(int postId) { ? ?postRepository.listenForLikes(postId).listen((likes) { ? ? ?likesStreamController.add(likes); ? }); } } ? ? @GenerateMocks([PostRepository]) void main() { ?MockPostRepository mockPostRepository = MockPostRepository(); ?late PostViewModel postViewModel; ? ?setUp(() { ? ?postViewModel = PostViewModel(mockPostRepository); }); ? ?test('Listen for likes works correctly', () { ? ?final mocklikesStreamController = StreamController<int>(); ? ? ?when(mockPostRepository.listenForLikes(any)) ? ? ? .thenAnswer((inv) => mocklikesStreamController.stream); ? ? ?postViewModel.listenForLikes(1); ? ? ?mocklikesStreamController.add(3); ? ?mocklikesStreamController.add(5); ? ?mocklikesStreamController.add(9); ? ? ?// checks if listen for likes is called ? ?verify(mockPostRepository.listenForLikes(1)); ? ?expect(postViewModel.likesStreamController.stream, emitsInOrder([3, 5, 9])); }); }
在上面的示例中,我們添加了一個(gè)listenforLikes方法,該方法調(diào)用該P(yáng)ostRepository方法并返回一個(gè)我們可以監(jiān)聽的流。接下來,我們創(chuàng)建了一個(gè)測試來偵聽流并檢查方法是否以正確的順序被調(diào)用和發(fā)出。
對(duì)于一些復(fù)雜的情況,我們可以使用expectLaterorexpectAsync1來代替只使用expect函數(shù)。
結(jié)論
就像大多數(shù)邏輯看起來一樣簡單,編寫測試非常重要,因此我們不會(huì)重復(fù) QA 這些功能。編寫測試的目的之一是在您的應(yīng)用程序變大時(shí)減少重復(fù)的 QA。文章來源:http://www.zghlxwxcb.cn/news/detail-474662.html
在本文中,我們了解了如何在編寫單元測試時(shí)有效地使用 Mockito 生成模擬。我們還學(xué)習(xí)了如何使用 fakes 和參數(shù)匹配器來編寫功能測試。文章來源地址http://www.zghlxwxcb.cn/news/detail-474662.html
到了這里,關(guān)于使用 Mockito 對(duì) Flutter 代碼進(jìn)行單元測試的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!