Skip to content

feat: basic testing framework #215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions design_system/lib/theme/app_text_styles.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//ignore_for_file: unused-files, unused-code

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';

const FontWeight _semiboldWeight = FontWeight.w600;

class AppTextStyles extends TextTheme {
final _isTesting = Platform.environment.containsKey('FLUTTER_TEST');

class AppTextStyles extends TextTheme {
const AppTextStyles({
super.headlineLarge,
super.headlineMedium,
Expand Down Expand Up @@ -45,12 +48,17 @@ class AppTextStyles extends TextTheme {
double fontSize,
FontWeight fontWeight,
) =>
GoogleFonts.roboto(
fontSize: fontSize,
fontWeight: fontWeight,
);
_isTesting
? TextStyle(fontSize: fontSize, fontWeight: fontWeight)
: GoogleFonts.roboto(
fontSize: fontSize,
fontWeight: fontWeight,
);

static AppTextStyles getDefaultAppStyles() => AppTextStyles.fromTextTheme(
static AppTextStyles getDefaultAppStyles() =>
_isTesting ? _testingTextTheme() : _appTextTheme();

static AppTextStyles _appTextTheme() => AppTextStyles.fromTextTheme(
textTheme: GoogleFonts.robotoTextTheme().copyWith(
labelLarge: _robotoTextStyle(20.sp, FontWeight.normal),
labelMedium: _robotoTextStyle(16.sp, FontWeight.normal),
Expand All @@ -60,6 +68,16 @@ class AppTextStyles extends TextTheme {
),
);

static AppTextStyles _testingTextTheme() => AppTextStyles.fromTextTheme(
textTheme: TextTheme(
labelLarge: _robotoTextStyle(20.sp, FontWeight.normal),
labelMedium: _robotoTextStyle(16.sp, FontWeight.normal),
labelSmall: _robotoTextStyle(14.sp, FontWeight.normal),
headlineMedium: _robotoTextStyle(20.sp, FontWeight.bold),
headlineLarge: _robotoTextStyle(24.sp, FontWeight.bold),
),
);

TextTheme getThemeData() => getDefaultAppStyles();
}

Expand Down
20 changes: 14 additions & 6 deletions design_system/lib/theme/custom_text_styles.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// ignore_for_file: overridden_fields

import 'dart:io';

import 'package:design_system/extensions/color_extensions.dart';
import 'package:design_system/theme/custom_colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:design_system/theme/custom_colors.dart';

const FontWeight _semiboldWeight = FontWeight.w600;

Expand Down Expand Up @@ -57,11 +59,17 @@ class CustomTextStyles extends ThemeExtension<CustomTextStyles> {
FontWeight fontWeight,
CustomColors customColors,
) =>
GoogleFonts.roboto(
fontSize: fontSize,
fontWeight: fontWeight,
color: customColors.textColor!.getShade(500),
);
Platform.environment.containsKey('FLUTTER_TEST')
? TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
color: customColors.textColor!.getShade(500),
)
: GoogleFonts.roboto(
fontSize: fontSize,
fontWeight: fontWeight,
color: customColors.textColor!.getShade(500),
);

@override
CustomTextStyles copyWith({MaterialColor? primary}) =>
Expand Down
3 changes: 2 additions & 1 deletion lib/core/common/config.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// ignore_for_file: constant_identifier_names

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter_template/core/common/environments.dart';
Expand All @@ -13,7 +14,7 @@ interface class Config {
static const String environmentFolder = 'environments';

static const debugMode = kDebugMode;

static bool testingMode = Platform.environment.containsKey('FLUTTER_TEST');
static late String apiBaseUrl;
static late String supabaseApiKey;
static late String appDirectoryPath;
Expand Down
2 changes: 1 addition & 1 deletion lib/core/di/di_utils_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class UtilsDiModule {

extension _GetItDiModuleExtensions on GetIt {
void _setupModule() {
registerLazySingleton(() => AppRouter(get()));
registerLazySingleton(() => AppRouter(sessionRepository: get()));
}
}
7 changes: 4 additions & 3 deletions lib/ui/router/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ part 'app_router.gr.dart';
class AppRouter extends _$AppRouter {
@override
final List<AutoRoute> routes;
final String? initialRoute;

ReevaluateListenable authReevaluateListenable;

AppRouter(SessionRepository sessionRepository)
AppRouter({required SessionRepository sessionRepository, this.initialRoute})
: authReevaluateListenable = ReevaluateListenable.stream(
sessionRepository.status.distinct().skip(1),
),
Expand All @@ -26,7 +27,7 @@ class AppRouter extends _$AppRouter {
path: '/',
guards: [UnauthenticatedGuard(sessionRepository)],
children: [
RedirectRoute(path: '', redirectTo: 'login'),
RedirectRoute(path: '', redirectTo: initialRoute ?? 'login'),
AutoRoute(path: 'login', page: SignInRoute.page),
],
),
Expand All @@ -35,7 +36,7 @@ class AppRouter extends _$AppRouter {
guards: [AuthenticatedGuard(sessionRepository)],
path: '/',
children: [
RedirectRoute(path: '', redirectTo: 'welcome'),
RedirectRoute(path: '', redirectTo: initialRoute ?? 'welcome'),
AutoRoute(path: 'welcome', page: WelcomeRoute.page),
],
),
Expand Down
4 changes: 2 additions & 2 deletions pubspec.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ dev_dependencies:
json_serializable: 6.8.0
lints: 4.0.0
mocktail: 1.0.3
path_provider_platform_interface: 2.1.2
plugin_platform_interface: 2.1.8

## TODO remove this when dart_code_linter updates the dependencies
dependency_overrides:
Expand Down
92 changes: 92 additions & 0 deletions test/mocks/mock_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'dart:async';

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_template/core/common/config.dart';
import 'package:flutter_template/core/common/logger.dart';
import 'package:flutter_template/core/di/di_provider.dart';
import 'package:flutter_template/core/source/common/http_service.dart';
import 'package:flutter_template/main.dart';
import 'package:flutter_template/ui/router/app_router.dart';
import 'package:hive/hive.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'services/http_service.dart';
import 'services/path_provider.dart';
import 'sources/secure_storage.dart';

class SimpleTesteableApp extends MyApp {
final Map<ApiOverrideKey, ResponseData>? apiOverrides;

final String initialRoute;
const SimpleTesteableApp._({
super.key,
this.apiOverrides,
this.initialRoute = '/',
}) : super();

static Future<SimpleTesteableApp> getUnauthenticatedApp({
Map<ApiOverrideKey, ResponseData>? apiOverrides,
String initialRoute = '/',
Map<String, String>? secureStorageData,
Map<String, Object>? sharedPreferencesData,
}) async {
await _initMockSdks(
apiOverrides: apiOverrides,
initialRoute: initialRoute,
sharedPreferenceData: secureStorageData,
secureStorageData: secureStorageData,
);
final app = SimpleTesteableApp._(
apiOverrides: apiOverrides,
initialRoute: initialRoute,
);
return app;
}
}

Future<void> _initMockSdks({
Map<ApiOverrideKey, ResponseData>? apiOverrides,
Map<String, String>? secureStorageData,
Map<String, Object>? sharedPreferenceData,
String? initialRoute,
}) async {
_initializeLocalSources(
secureStorageData: secureStorageData,
sharedPreferenceData: sharedPreferenceData,
);
await _initializeProviders(apiOverrides, initialRoute);
await Logger.init();
await Config.initialize();
Hive.init(Config.appDirectoryPath);
}

void _initializeLocalSources({
Map<String, String>? secureStorageData,
Map<String, Object>? sharedPreferenceData,
}) {
FakeFlutterSecureStorage.setInitialData(secureStorageData ?? {});
PathProviderPlatform.instance = FakePathProviderPlatform();
SharedPreferences.setMockInitialValues(sharedPreferenceData ?? {});
}

Future<void> _initializeProviders(
Map<ApiOverrideKey, ResponseData>? apiOverrides,
String? initialRoute,
) async {
await DiProvider.init();
DiProvider.instance
..allowReassignment = true
..registerLazySingleton<FlutterSecureStorage>(
() => FakeFlutterSecureStorage(),
)
..registerLazySingleton<HttpService>(
() => FakeHttpService()..mockApi(apiOverrides ?? {}),
)
..registerLazySingleton<AppRouter>(
() => AppRouter(
initialRoute: initialRoute,
sessionRepository: DiProvider.get(),
),
);
}
72 changes: 72 additions & 0 deletions test/mocks/services/http_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:dio/dio.dart';
import 'package:flutter_template/core/source/common/http_service.dart';
import 'package:mocktail/mocktail.dart';

class FakeHttpService extends Mock implements HttpServiceDio {}

typedef ResponseData = Map<String, dynamic>;
typedef ApiOverrideKey = ({String path, ApiMethod method});

enum ApiMethod { apiPost, apiGet, apiDelete, apiPut }

extension MockApiServiceExtension on FakeHttpService {
void mockApi(Map<ApiOverrideKey, ResponseData> apiOverrides) {
for (final element in apiOverrides.entries) {
switch (element.key.method) {
case ApiMethod.apiPost:
_mockPost(element.key.path, element.value);
case ApiMethod.apiGet:
_mockGet(element.key.path, element.value);
case ApiMethod.apiDelete:
_mockDelete(element.key.path, element.value);
case ApiMethod.apiPut:
_mockPut(element.key.path, element.value);
}
}
}

void _mockPost(
String path,
Map<String, dynamic> response, {
Map<String, dynamic>? parameters,
}) =>
when(() => post(path, queryParameters: parameters)).thenAnswer(
(_) async => Response(
requestOptions: RequestOptions(path: path),
data: response,
),
);
void _mockGet(
String path,
Map<String, dynamic> response, {
Map<String, dynamic>? parameters,
}) =>
when(() => get(path, queryParameters: parameters)).thenAnswer(
(_) async => Response(
requestOptions: RequestOptions(path: path),
data: response,
),
);
void _mockDelete(
String path,
Map<String, dynamic> response, {
Map<String, dynamic>? parameters,
}) =>
when(() => delete(path, queryParameters: parameters)).thenAnswer(
(_) async => Response(
requestOptions: RequestOptions(path: path),
data: response,
),
);
void _mockPut(
String path,
Map<String, dynamic> response, {
Map<String, dynamic>? parameters,
}) =>
when(() => put(path, queryParameters: parameters)).thenAnswer(
(_) async => Response(
requestOptions: RequestOptions(path: path),
data: response,
),
);
}
44 changes: 44 additions & 0 deletions test/mocks/services/path_provider.dart
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

const String kTemporaryPath = 'temporaryPath';
const String kApplicationSupportPath = 'applicationSupportPath';
const String kDownloadsPath = 'downloadsPath';
const String kLibraryPath = 'libraryPath';
const String kApplicationDocumentsPath = 'applicationDocumentsPath';
const String kExternalCachePath = 'externalCachePath';
const String kExternalStoragePath = 'externalStoragePath';

class FakePathProviderPlatform extends Fake
with MockPlatformInterfaceMixin
implements PathProviderPlatform {
@override
Future<String?> getTemporaryPath() async => kTemporaryPath;

@override
Future<String?> getApplicationSupportPath() async => kApplicationSupportPath;

@override
Future<String?> getLibraryPath() async => kLibraryPath;

@override
Future<String?> getApplicationDocumentsPath() async =>
kApplicationDocumentsPath;

@override
Future<String?> getExternalStoragePath() async => kExternalStoragePath;

@override
Future<List<String>?> getExternalCachePaths() async =>
<String>[kExternalCachePath];

@override
Future<List<String>?> getExternalStoragePaths({
StorageDirectory? type,
}) async =>
<String>[kExternalStoragePath];

@override
Future<String?> getDownloadsPath() async => kDownloadsPath;
}
7 changes: 7 additions & 0 deletions test/mocks/sources/secure_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class FakeFlutterSecureStorage extends FlutterSecureStorage {
static void setInitialData(Map<String, String> initialData) {
FlutterSecureStorage.setMockInitialValues(initialData);
}
}
Loading
Loading