From 372f5611fefae47000cae1646733d902b33ed4d0 Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Tue, 27 Sep 2022 21:46:30 +0100 Subject: [PATCH] Unit tests added --- lib/Home.dart | 4 +- lib/locator.dart | 1 + pubspec.lock | 7 ++ pubspec.yaml | 2 + test/resources/success_call_api.json | 13 ++ test/resources/success_call_backup_api.json | 8 ++ test/resources/test_res.env | 2 + test/test_utils/test_utils.dart | 8 ++ test/unit_test/repository_test.dart | 124 ++++++++++++++++++ test/unit_test/viewmodel_test.dart | 132 ++++++++++++++++++++ 10 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 test/resources/success_call_api.json create mode 100644 test/resources/success_call_backup_api.json create mode 100644 test/resources/test_res.env create mode 100644 test/test_utils/test_utils.dart create mode 100644 test/unit_test/repository_test.dart create mode 100644 test/unit_test/viewmodel_test.dart diff --git a/lib/Home.dart b/lib/Home.dart index aa84cae..400d048 100644 --- a/lib/Home.dart +++ b/lib/Home.dart @@ -34,7 +34,7 @@ class HomePage extends BaseStatelessWidget { model.setConversionPair(selected1, selected2); }), ConverterEditText("Enter conversion from", controller1, (input) => { - controller2.text = model.convertInput(input, SelectionType.conversionFrom) + controller2.text = model.convertInput(input?.trim(), SelectionType.conversionFrom) }) ], ), @@ -48,7 +48,7 @@ class HomePage extends BaseStatelessWidget { model.setConversionPair(selected1, selected2); }), ConverterEditText("Enter conversion from", controller2, (input) => { - controller1.text = model.convertInput(input, SelectionType.conversionTo) + controller1.text = model.convertInput(input?.trim(), SelectionType.conversionTo) }) ], ), diff --git a/lib/locator.dart b/lib/locator.dart index ca4ccfa..ab9041e 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -11,6 +11,7 @@ GetIt locator = GetIt.instance; void setupLocator() { final dio = Dio(); + locator.registerLazySingleton(() => PreferenceProvider()); locator.registerLazySingleton(() => CurrencyApi(dio)); locator.registerLazySingleton(() => BackupCurrencyApi(dio)); diff --git a/pubspec.lock b/pubspec.lock index 110524a..d576ccc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -366,6 +366,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + mockito: + dependency: "direct main" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.2" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b37e696..6e3d8f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: retrofit: ^3.0.1+1 logger: ^1.1.0 flutter_dotenv: ^5.0.2 + mockito: ^5.3.2 dev_dependencies: flutter_test: @@ -76,6 +77,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - .env + - test/resources/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/test/resources/success_call_api.json b/test/resources/success_call_api.json new file mode 100644 index 0000000..aff2cb9 --- /dev/null +++ b/test/resources/success_call_api.json @@ -0,0 +1,13 @@ +{ + "query": { + "count": 1 + }, + "results": { + "AUD_GBP": { + "id": "AUD_GBP", + "fr": "AUD", + "to": "GBP", + "val": 0.601188 + } + } +} \ No newline at end of file diff --git a/test/resources/success_call_backup_api.json b/test/resources/success_call_backup_api.json new file mode 100644 index 0000000..3bbed2a --- /dev/null +++ b/test/resources/success_call_backup_api.json @@ -0,0 +1,8 @@ +{ + "amount": 1.0, + "base": "AUD", + "date": "2022-09-23", + "rates": { + "GBP": 0.59483 + } +} \ No newline at end of file diff --git a/test/resources/test_res.env b/test/resources/test_res.env new file mode 100644 index 0000000..3fef03e --- /dev/null +++ b/test/resources/test_res.env @@ -0,0 +1,2 @@ +// freecurrencyapi api key +apiKey=12121 \ No newline at end of file diff --git a/test/test_utils/test_utils.dart b/test/test_utils/test_utils.dart new file mode 100644 index 0000000..c1a070e --- /dev/null +++ b/test/test_utils/test_utils.dart @@ -0,0 +1,8 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +Future> readJson(String filePath) async { + final String response = await rootBundle.loadString('$filePath.json'); + return await json.decode(response); +} \ No newline at end of file diff --git a/test/unit_test/repository_test.dart b/test/unit_test/repository_test.dart new file mode 100644 index 0000000..6831e92 --- /dev/null +++ b/test/unit_test/repository_test.dart @@ -0,0 +1,124 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:easy_cc_flutter/data/model/Currency.dart'; +import 'package:easy_cc_flutter/data/network/backupCurrencyApi.dart'; +import 'package:easy_cc_flutter/data/network/currencyApi.dart'; +import 'package:easy_cc_flutter/data/prefs/CurrencyPair.dart'; +import 'package:easy_cc_flutter/data/prefs/PreferenceProvider.dart'; +import 'package:easy_cc_flutter/data/repository/RepositoryImpl.dart'; +import 'package:easy_cc_flutter/locator.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:retrofit/dio.dart' as http; + +import '../test_utils/test_utils.dart'; +import 'repository_test.mocks.dart'; + +@GenerateMocks([ + PreferenceProvider, + CurrencyApi, + BackupCurrencyApi +], customMocks: [ + MockSpec>( + onMissingStub: OnMissingStub.returnDefault), + MockSpec>( + as: #MockCurrencyResponse, onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault) +]) +void main() { + late RepositoryImpl repository; + late PreferenceProvider preferenceProvider; + late CurrencyApi currencyApi; + late BackupCurrencyApi backupCurrencyApi; + + const String fromCurrency = "AUD - Australian Dollar"; + const String toCurrency = "GBP - British Pound Sterling"; + + setUpAll(() async { + // Setup + await dotenv.load(fileName: "test/resources/test_res.env"); + // Create mock object. + preferenceProvider = MockPreferenceProvider(); + currencyApi = MockCurrencyApi(); + backupCurrencyApi = MockBackupCurrencyApi(); + + locator.registerLazySingleton(() => preferenceProvider); + locator.registerLazySingleton(() => currencyApi); + locator.registerLazySingleton(() => backupCurrencyApi); + + repository = RepositoryImpl(); + }); + + test('get currency pair from prefs', () { + // Given + CurrencyPair pair = CurrencyPair(fromCurrency, toCurrency); + + // When + when(preferenceProvider.getConversionPair()).thenReturn(pair); + + // Then + expect(repository.getConversionPair(), pair); + }); + + test('get currency rate from API', () async { + // Given + http.HttpResponse mockResponse = MockHttpResponse(); + ResponseObject responseObject = ResponseObject.fromJson( + await readJson("test/resources/success_call_api")); + Currency currencyObject = Currency("AUD", "GBP", 0.601188); + String currency = "AUD_GBP"; + + // When + when(mockResponse.data).thenReturn(responseObject); + when(currencyApi.getConversion(dotenv.env['apiKey']!, currency)) + .thenAnswer((_) async => mockResponse); + + // Then + Currency retrieved = + await repository.getConversationRateFromApi(fromCurrency, toCurrency); + expect(retrieved.toString(), currencyObject.toString()); + }); + + test('get currency rate from backup API', () async { + // Given + http.HttpResponse mockResponse = MockCurrencyResponse(); + CurrencyResponse currencyResponse = CurrencyResponse.fromJson( + await readJson("test/resources/success_call_backup_api")); + Currency currencyObject = Currency("AUD", "GBP", 0.601188); + String currency = "AUD_GBP"; + + // When + when(currencyApi.getConversion(dotenv.env['apiKey']!, currency)) + .thenAnswer((_) async => Future.error(MockDioError())); + when(mockResponse.data).thenReturn(currencyResponse); + when(backupCurrencyApi.getCurrencyRate("AUD", "GBP")) + .thenAnswer((_) async => mockResponse); + + // Then + Currency retrieved = + await repository.getConversationRateFromApi(fromCurrency, toCurrency); + expect(retrieved.toString(), currencyObject.toString()); + }); + + test('unable to retrieve rate from both APIs', () async { + // Given + String currency = "AUD_GBP"; + DioError backUpError = MockDioError(); + + // When + when(backUpError.error).thenReturn("Error message"); + when(currencyApi.getConversion(dotenv.env['apiKey']!, currency)) + .thenAnswer((_) async => Future.error(MockDioError())); + when(backupCurrencyApi.getCurrencyRate("AUD", "GBP")) + .thenAnswer((_) async => Future.error(backUpError)); + + // Then + expect(() async => await repository.getConversationRateFromApi(fromCurrency, toCurrency), + throwsA(predicate((e) => + e is HttpException && + e.message == 'Error message'))); + }); +} diff --git a/test/unit_test/viewmodel_test.dart b/test/unit_test/viewmodel_test.dart new file mode 100644 index 0000000..a792d36 --- /dev/null +++ b/test/unit_test/viewmodel_test.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:easy_cc_flutter/MainViewModel.dart'; +import 'package:easy_cc_flutter/Utils/SelectionType.dart'; +import 'package:easy_cc_flutter/data/model/Currency.dart'; +import 'package:easy_cc_flutter/data/prefs/CurrencyPair.dart'; +import 'package:easy_cc_flutter/data/repository/RepositoryImpl.dart'; +import 'package:easy_cc_flutter/locator.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:easy_cc_flutter/Utils/ViewState.dart'; + +import 'viewmodel_test.mocks.dart'; + +@GenerateMocks([ + RepositoryImpl +]) +void main() { + late MainViewModel mainViewModel; + late RepositoryImpl repository; + + const String fromCurrency = "AUD - Australian Dollar"; + const String toCurrency = "GBP - British Pound Sterling"; + + setUpAll(() async { + // Setup + await dotenv.load(fileName: "test/resources/test_res.env"); + // Create mock object. + repository = MockRepositoryImpl(); + + locator.registerLazySingleton(() => repository); + + mainViewModel = MainViewModel(); + }); + + test('get currency pair from prefs', () { + // Given + CurrencyPair pair = CurrencyPair(fromCurrency, toCurrency); + + // When + when(repository.getConversionPair()).thenReturn(pair); + + // Then + String fromSelection = mainViewModel.getConversionPair(SelectionType.conversionFrom); + String toSelection = mainViewModel.getConversionPair(SelectionType.conversionTo); + expect(fromSelection, fromCurrency); + expect(toSelection, toCurrency); + }); + + test('get currency pair from prefs nothing stored', () { + // Given + CurrencyPair pair = CurrencyPair(null, null); + + // When + when(repository.getConversionPair()).thenReturn(pair); + + // Then + String fromSelection = mainViewModel.getConversionPair(SelectionType.conversionFrom); + String toSelection = mainViewModel.getConversionPair(SelectionType.conversionTo); + expect(fromSelection, "ALL - Albanian Lek"); + expect(toSelection, "ALL - Albanian Lek"); + }); + + test('set the currency rate from API', () async{ + // Given + Currency currency = Currency(fromCurrency, toCurrency, 0.6); + + // When + when(repository.getConversationRateFromApi(fromCurrency, toCurrency)) + .thenAnswer((_) async => currency); + + // Then + mainViewModel.setCurrencyRate(fromCurrency, toCurrency); + await Future.delayed(const Duration(milliseconds: 100)); + + expect((mainViewModel.viewState as HasData).data, currency); + expect(mainViewModel.conversionRate, currency.rate); + }); + + test('currency rate api fails to retrieve data', () async{ + // Given + String errorMessage = "failed to retrieve data"; + + // When + when(repository.getConversationRateFromApi(fromCurrency, toCurrency)) + .thenAnswer((_) async => Future.error(HttpException(errorMessage))); + + // Then + mainViewModel.setCurrencyRate(fromCurrency, toCurrency); + await Future.delayed(const Duration(milliseconds: 100)); + + expect((mainViewModel.viewState as HasError).error, errorMessage); + }); + + test('convert input with correct format', () async{ + // Given + String input = "43"; + mainViewModel.conversionRate = 6.34; + + // When + + // Then + expect(mainViewModel.convertInput(input, SelectionType.conversionFrom), "272.62"); + expect(mainViewModel.convertInput(input, SelectionType.conversionTo), "6.78"); + }); + + test('convert input with empty input', () async{ + // Given + String input = ""; + mainViewModel.conversionRate = 6.34; + + // When + + // Then + expect(mainViewModel.convertInput(input, SelectionType.conversionFrom), ""); + expect(mainViewModel.convertInput(input, SelectionType.conversionTo), ""); + }); + + test('convert input with empty input', () async{ + // Given + String input = "45.45564"; + mainViewModel.conversionRate = 6.34; + + // When + + // Then + expect(mainViewModel.convertInput(input, SelectionType.conversionFrom), "288.19"); + expect(mainViewModel.convertInput(input, SelectionType.conversionTo), "7.17"); + }); +} \ No newline at end of file