From 5572c66b10045f2d94c43be0a9c37a9e53a451e2 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Sun, 21 Sep 2025 11:03:06 +0200 Subject: [PATCH] Integrate logging system into Flutter app with service registration and testing setup --- finlog_app/app/lib/main.dart | 14 +++- finlog_app/app/pubspec.yaml | 2 + finlog_app/fluttery/lib/fluttery.dart | 64 +++++++++++++++-- finlog_app/fluttery/lib/logger/logger.dart | 33 +++++++++ .../fluttery/lib/src/logger/logger_impl.dart | 57 +++++++++++++++ finlog_app/fluttery/pubspec.yaml | 3 + finlog_app/fluttery/test/fluttery_test.dart | 39 +++++++++-- .../fluttery/test/logger/logger_test.dart | 69 +++++++++++++++++++ finlog_app/fluttery/test/mocks/mocks.dart | 4 ++ 9 files changed, 273 insertions(+), 12 deletions(-) create mode 100644 finlog_app/fluttery/lib/logger/logger.dart create mode 100644 finlog_app/fluttery/lib/src/logger/logger_impl.dart create mode 100644 finlog_app/fluttery/test/logger/logger_test.dart create mode 100644 finlog_app/fluttery/test/mocks/mocks.dart diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart index 7b7f5b6..5828ebb 100644 --- a/finlog_app/app/lib/main.dart +++ b/finlog_app/app/lib/main.dart @@ -1,6 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:fluttery/fluttery.dart'; +import 'package:fluttery/logger/logger.dart'; + +Future main() async { + // Ensures that the Flutter engine and widget binding + // are initialized before using async services or plugins + WidgetsFlutterBinding.ensureInitialized(); + + // any services + App.registerDefaultServices(); + + final logger = App.service(); + logger.debug("[MAIN] Registered all default services"); -void main() { runApp(const MyApp()); } diff --git a/finlog_app/app/pubspec.yaml b/finlog_app/app/pubspec.yaml index 09d2538..4a153ec 100644 --- a/finlog_app/app/pubspec.yaml +++ b/finlog_app/app/pubspec.yaml @@ -31,6 +31,8 @@ environment: dependencies: flutter: sdk: flutter + fluttery: + path: ../fluttery # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/finlog_app/fluttery/lib/fluttery.dart b/finlog_app/fluttery/lib/fluttery.dart index 298576d..9d993e4 100644 --- a/finlog_app/fluttery/lib/fluttery.dart +++ b/finlog_app/fluttery/lib/fluttery.dart @@ -1,5 +1,61 @@ -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; +library; + +import 'package:fluttery/logger/logger.dart'; +import 'package:fluttery/src/logger/logger_impl.dart'; +import 'package:kiwi/kiwi.dart'; + +/// A class to manage services. +class App { + static final _AppService _appService = _AppService(); + + /// Registers a service with a factory function to instantiate the implementation. + /// + /// This ensures that the implementation is created when the service is requested. + /// + /// `implFactory` - A factory method to create the service implementation. + static void registerService(T Function() implFactory) { + _appService.registerSingleton(implFactory); + } + + /// Retrieves the registered service. + /// + /// Returns an instance of the registered service. + static T service() { + return _appService.resolve(); + } + + /// Registers the default services required by the application. + static void registerDefaultServices() { + registerService(() => LoggerImpl()); + } +} + +/// Abstract class to represent a service. +abstract class Service {} + +/// Internal class to manage the registration and resolution of services. +class _AppService { + static _AppService? _singleton; + + static final KiwiContainer _kiwi = KiwiContainer(); + + /// Factory constructor to ensure singleton instance of _AppService. + factory _AppService() => _singleton ??= _AppService._(); + + /// Private constructor. + _AppService._(); + + /// Registers a singleton service with a factory function to create the instance. + /// + /// `serviceFactory` - A factory method to create the service implementation. + void registerSingleton(T Function() serviceFactory) { + _kiwi.registerFactory((c) => serviceFactory()); + } + + /// Resolves and retrieves the registered service. + /// + /// Returns an instance of the registered service. + T resolve() { + return _kiwi.resolve(); + } } diff --git a/finlog_app/fluttery/lib/logger/logger.dart b/finlog_app/fluttery/lib/logger/logger.dart new file mode 100644 index 0000000..ee1f438 --- /dev/null +++ b/finlog_app/fluttery/lib/logger/logger.dart @@ -0,0 +1,33 @@ +import 'package:fluttery/fluttery.dart'; +import 'package:logging/logging.dart' as lib; + +/// Abstract class for logging service. +/// Provides methods for different log levels and configuration. +abstract class Logger extends Service { + /// Logs an informational message. + /// + /// [message] is the information to log. + void info(String message); + + /// Logs a warning message. + /// + /// [message] is the warning to log. + void warning(String message); + + /// Logs an error message with optional error and stack trace. + /// + /// [message] is the error message to log. + /// [error] is the optional error object associated with this log entry. + /// [stackTrace] is the optional stack trace associated with this log entry. + void error(String message, [Object? error, StackTrace? stackTrace]); + + /// Logs a debug message. + /// + /// [message] is the debug message to log. + void debug(String message); + + /// Sets the log level for the logger. + /// + /// [level] is the new log level to set. + void setLogLevel(lib.Level level); +} diff --git a/finlog_app/fluttery/lib/src/logger/logger_impl.dart b/finlog_app/fluttery/lib/src/logger/logger_impl.dart new file mode 100644 index 0000000..5d8a41d --- /dev/null +++ b/finlog_app/fluttery/lib/src/logger/logger_impl.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:fluttery/logger/logger.dart'; +import 'package:logging/logging.dart' as lib; + +// ignore_for_file: avoid_print +class LoggerImpl implements Logger { + final lib.Logger _logger; + + // coverage:ignore-start + /// Constructor + LoggerImpl() : _logger = lib.Logger("Logger") { + _logger.onRecord.listen((lib.LogRecord record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + if (record.error != null) { + print('Error: ${record.error}'); + } + if (record.stackTrace != null) { + print('Stack Trace: ${record.stackTrace}'); + } + }); + } + + // coverage:ignore-end + @visibleForTesting + factory LoggerImpl.forTest(lib.Logger logger) { + final instance = LoggerImpl._internal(logger); + return instance; + } + + // Private internal constructor + LoggerImpl._internal(this._logger); + + @override + void info(String message) { + _logger.info(message); + } + + @override + void warning(String message) { + _logger.warning(message); + } + + @override + void error(String message, [Object? error, StackTrace? stackTrace]) { + _logger.severe(message, error, stackTrace); + } + + @override + void debug(String message) { + _logger.fine(message); + } + + @override + void setLogLevel(lib.Level level) { + _logger.level = level; + } +} diff --git a/finlog_app/fluttery/pubspec.yaml b/finlog_app/fluttery/pubspec.yaml index 6c6bdb1..316de94 100644 --- a/finlog_app/fluttery/pubspec.yaml +++ b/finlog_app/fluttery/pubspec.yaml @@ -11,6 +11,9 @@ environment: dependencies: flutter: sdk: flutter + kiwi: ^5.0.1 + logging: ^1.3.0 + mocktail: ^1.0.4 dev_dependencies: flutter_test: diff --git a/finlog_app/fluttery/test/fluttery_test.dart b/finlog_app/fluttery/test/fluttery_test.dart index be69fe2..b2939f1 100644 --- a/finlog_app/fluttery/test/fluttery_test.dart +++ b/finlog_app/fluttery/test/fluttery_test.dart @@ -1,12 +1,37 @@ -import 'package:flutter_test/flutter_test.dart'; - import 'package:fluttery/fluttery.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fluttery/logger/logger.dart'; +import 'package:fluttery/src/logger/logger_impl.dart'; +import 'package:kiwi/kiwi.dart'; + +import 'mocks/mocks.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('App Service Tests', () { + // Clear the singleton state before each test to ensure isolation + setUp(() { + // KiwiContainer provides a clear method to remove all registered services + KiwiContainer().clear(); + }); + + test('should register and resolve a custom service', () { + // Register a mock logger service + final mockLogger = MockLogger(); + App.registerService(() => mockLogger); + + // Resolve the service and check if it's the same instance + final resolvedLogger = App.service(); + expect(resolvedLogger, isA()); + expect(resolvedLogger, same(mockLogger)); + }); + + test('should register and resolve default services', () { + // Register default services + App.registerDefaultServices(); + + // Resolve the Logger service and check if it's an instance of LoggerImpl + final logger = App.service(); + expect(logger, isA()); + }); }); } diff --git a/finlog_app/fluttery/test/logger/logger_test.dart b/finlog_app/fluttery/test/logger/logger_test.dart new file mode 100644 index 0000000..60ac934 --- /dev/null +++ b/finlog_app/fluttery/test/logger/logger_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:logging/logging.dart' as lib; +import 'package:fluttery/src/logger/logger_impl.dart'; + +// Mock class for the lib.Logger +class MockLibLogger extends Mock implements lib.Logger {} + +void main() { + final mockLibLogger = MockLibLogger(); + + group('LoggerImpl', () { + late LoggerImpl loggerImpl; + + setUp(() { + loggerImpl = LoggerImpl.forTest(mockLibLogger); + }); + + test('info method logs an info message', () { + loggerImpl.info('Info message'); + + // Verify that the info method was called on the mock logger with the correct message + verify(() => mockLibLogger.info('Info message')).called(1); + }); + + test('warning method logs a warning message', () { + loggerImpl.warning('Warning message'); + + // Verify that the warning method was called on the mock logger with the correct message + verify(() => mockLibLogger.warning('Warning message')).called(1); + }); + + test('error method logs an error message with optional parameters', () { + final exception = Exception('Test exception'); + final stackTrace = StackTrace.current; + + loggerImpl.error('Error message', exception, stackTrace); + + // Verify that the severe method was called on the mock logger with the correct parameters + verify( + () => mockLibLogger.severe('Error message', exception, stackTrace), + ).called(1); + }); + + test('debug method logs a debug message', () { + loggerImpl.debug('Debug message'); + + // Verify that the fine method was called on the mock logger with the correct message + verify(() => mockLibLogger.fine('Debug message')).called(1); + }); + + test('setLogLevel method sets the logger level', () { + // This is to capture the change in the logger level + var capturedLevel = lib.Level.INFO; + + // Capture the setter call + when(() => mockLibLogger.level = any()).thenAnswer((invocation) { + capturedLevel = invocation.positionalArguments.first as lib.Level; + return capturedLevel; + }); + + // Set the log level + loggerImpl.setLogLevel(lib.Level.WARNING); + + // Verify that the logger level is set to the expected level + expect(capturedLevel, lib.Level.WARNING); + }); + }); +} diff --git a/finlog_app/fluttery/test/mocks/mocks.dart b/finlog_app/fluttery/test/mocks/mocks.dart new file mode 100644 index 0000000..2602ab6 --- /dev/null +++ b/finlog_app/fluttery/test/mocks/mocks.dart @@ -0,0 +1,4 @@ +import 'package:fluttery/logger/logger.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockLogger extends Mock implements Logger {}