From 01c3ceaa8253a898c0984647cae4f3ff2882cd51 Mon Sep 17 00:00:00 2001 From: Wim Van Laer Date: Sat, 4 Nov 2023 23:32:03 +0100 Subject: [PATCH] feat!: upgrade to dart 3 + better usage of `FutureOr` capabilities (#11) --- .idea/misc.xml | 1 - CHANGELOG.md | 6 + example/behaviour_example.dart | 8 +- example/behaviour_without_input_example.dart | 2 +- lib/behaviour.dart | 2 + lib/src/behaviour.dart | 17 +- lib/src/behaviour_interface.dart | 6 +- lib/src/behaviour_mixin.dart | 51 +++- lib/src/behaviour_without_input.dart | 17 +- .../behaviour_without_input_interface.dart | 6 +- lib/src/exception_or.dart | 112 +------ lib/src/exception_or_extensions.dart | 72 +++++ lib/src/future_or_extensions.dart | 13 + pubspec.yaml | 6 +- test/src/behaviour_mixin_test.dart | 59 +++- test/src/behaviour_mixin_test.types.dart | 24 +- test/src/behaviour_test.dart | 17 +- test/src/behaviour_test.types.dart | 19 +- test/src/behaviour_track_test.types.dart | 2 +- test/src/behaviour_without_input_test.dart | 17 +- .../behaviour_without_input_test.types.dart | 19 +- test/src/exception_or_extensions_test.dart | 214 ++++++++++++++ test/src/exception_or_test.dart | 277 +++--------------- test/src/future_or_extensions_test.dart | 33 +++ 24 files changed, 602 insertions(+), 398 deletions(-) create mode 100644 lib/src/exception_or_extensions.dart create mode 100644 lib/src/future_or_extensions.dart create mode 100644 test/src/exception_or_extensions_test.dart create mode 100644 test/src/future_or_extensions_test.dart diff --git a/.idea/misc.xml b/.idea/misc.xml index 639900d..6e86672 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/CHANGELOG.md b/CHANGELOG.md index 2726df4..71522a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.0 + +* feat!: upgraded to dart v3 + - removed `whenSuccess` and `whenFailed` methods + variants from `ExceptionOr` +* feat: better usage of `FutureOr` functionality + ## 2.1.1 * ci stuff diff --git a/example/behaviour_example.dart b/example/behaviour_example.dart index 3241a5a..c9d90cf 100644 --- a/example/behaviour_example.dart +++ b/example/behaviour_example.dart @@ -1,18 +1,19 @@ import 'dart:async'; +import 'dart:developer'; import 'package:behaviour/behaviour.dart'; class CreateCustomer extends Behaviour { @override Future action(CreateCustomerParams input, BehaviourTrack? track) { - // TODO logic to create a customer + log('TODO create customer: ${input.name} - ${input.phoneNumber}'); return Future.value(); } @override FutureOr onCatchError( Object e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { return Exception('An unknown error occurred: $e'); @@ -22,12 +23,10 @@ class CreateCustomer extends Behaviour { class CreateCustomerParams { const CreateCustomerParams({ required this.name, - required this.address, required this.phoneNumber, }); final String name; - final String address; final String phoneNumber; } @@ -36,7 +35,6 @@ Future main() async { await createCustomer( const CreateCustomerParams( name: 'Wim', - address: 'Somewhere in Belgium', phoneNumber: '+32 xxx xx xx xx', ), ); diff --git a/example/behaviour_without_input_example.dart b/example/behaviour_without_input_example.dart index 9ebb667..8aee908 100644 --- a/example/behaviour_without_input_example.dart +++ b/example/behaviour_without_input_example.dart @@ -29,6 +29,6 @@ Future main() async { final getProfileData = GetProfileData(); await getProfileData().thenWhen( (exception) => log('Exception: $exception'), - (value) => log('value: $value'), + (value) => log('value: ${value.name} - ${value.birthday}'), ); } diff --git a/lib/behaviour.dart b/lib/behaviour.dart index f971ffe..4b7fbda 100644 --- a/lib/behaviour.dart +++ b/lib/behaviour.dart @@ -10,3 +10,5 @@ export 'src/behaviour_monitor.dart'; export 'src/behaviour_without_input.dart'; export 'src/behaviour_without_input_interface.dart'; export 'src/exception_or.dart'; +export 'src/exception_or_extensions.dart'; +export 'src/future_or_extensions.dart'; diff --git a/lib/src/behaviour.dart b/lib/src/behaviour.dart index 2e5f8d2..891aee5 100644 --- a/lib/src/behaviour.dart +++ b/lib/src/behaviour.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:behaviour/behaviour.dart'; /// A [Behaviour] is a type which only has one function, it's behaviour. It is @@ -15,16 +17,19 @@ abstract class Behaviour extends BehaviourBase implements BehaviourInterface { /// Super does not need to be called by it's implementers. It only sets the /// [monitor] by which the behaviour can be monitored. - Behaviour({ - BehaviourMonitor? monitor, - }) : super(monitor: monitor); + Behaviour({super.monitor}); /// [call] executes the action of the behaviour. If the action is successful, /// the return value is wrapped in a [Success] else the exception is wrapped /// in a [Failed]. @override - Future> call(TIn input) { - return executeAction((track) async => Success(await action(input, track))); + FutureOr> call(TIn input) { + return executeAction((track) { + return action(input, track).whenFutureOrValue( + (future) => future.then((result) => Success(result)), + (result) => Success(result), + ); + }); } /// [action] contains the actual logic of the behaviour. @@ -33,5 +38,5 @@ abstract class Behaviour extends BehaviourBase /// [BehaviourTrack.end], [BehaviourTrack.stopWithException] and /// [BehaviourTrack.stopWithError] are called from the super class. @override - Future action(TIn input, BehaviourTrack? track); + FutureOr action(TIn input, BehaviourTrack? track); } diff --git a/lib/src/behaviour_interface.dart b/lib/src/behaviour_interface.dart index 8814b7d..924c59e 100644 --- a/lib/src/behaviour_interface.dart +++ b/lib/src/behaviour_interface.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:behaviour/behaviour.dart'; /// A [BehaviourInterface] is a type which only has one function, it's @@ -15,7 +17,7 @@ abstract class BehaviourInterface { /// [call] executes the action of the behaviour. If the action is successful, /// the return value is wrapped in a [Success] else the exception is wrapped /// in a [Failed]. - Future> call(TIn input); + FutureOr> call(TIn input); /// [action] contains the actual logic of the behaviour. /// @@ -25,5 +27,5 @@ abstract class BehaviourInterface { /// [track] can be used for monitoring. The [BehaviourTrack.start], /// [BehaviourTrack.end], [BehaviourTrack.stopWithException] and /// [BehaviourTrack.stopWithError] are called from the super class. - Future action(TIn input, BehaviourTrack? track); + FutureOr action(TIn input, BehaviourTrack? track); } diff --git a/lib/src/behaviour_mixin.dart b/lib/src/behaviour_mixin.dart index eb340a8..d09e13c 100644 --- a/lib/src/behaviour_mixin.dart +++ b/lib/src/behaviour_mixin.dart @@ -33,22 +33,49 @@ mixin BehaviourMixin { /// is added to the track. /// /// When something is caught in the try catch, the [onCatch] method is called - /// with the caught object, stacktrace and track as parameters. The result + /// with the caught object, stackTrace and track as parameters. The result /// of this [onCatch] method is then wrapped within a [Failed] and is returned. - Future> executeAction(Action action) async { + FutureOr> executeAction(Action action) { final track = monitor?.createBehaviourTrack(this); try { track?.start(); - final either = await action(track); - track?.end(); - return either; - } on Exception catch (exception, stackTrace) { - return Failed(await onCatchException(exception, stackTrace, track)); + final futureOr = action(track); + if (futureOr is ExceptionOr) { + track?.end(); + return futureOr; + } + return _catchFutureError(futureOr, track); } catch (error, stackTrace) { - return Failed(await onCatchError(error, stackTrace, track)); + return _catch(error, stackTrace, track); } } + Future> _catchFutureError( + Future> future, + BehaviourTrack? track, + ) async { + try { + return await future; + } catch (error, stackTrace) { + return _catch(error, stackTrace, track); + } + } + + FutureOr> _catch( + Object error, + StackTrace stackTrace, + BehaviourTrack? track, + ) { + final futureOrException = error is Exception + ? onCatchException(error, stackTrace, track) + : onCatchError(error, stackTrace, track); + + return futureOrException.whenFutureOrValue( + (future) => future.then((exception) => Failed(exception)), + (exception) => Failed(exception), + ); + } + /// Is invoked when an exception is caught by the try-catch block in /// [executeAction]. /// @@ -59,10 +86,10 @@ mixin BehaviourMixin { /// super method should be called to invoke it. FutureOr onCatchException( Exception exception, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { - track?.stopWithException(exception, stacktrace); + track?.stopWithException(exception, stackTrace); return exception; } @@ -76,10 +103,10 @@ mixin BehaviourMixin { /// super method should be called to invoke it. FutureOr onCatchError( Object error, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { - track?.stopWithError(error, stacktrace); + track?.stopWithError(error, stackTrace); if (error is Exception) { return error; } else { diff --git a/lib/src/behaviour_without_input.dart b/lib/src/behaviour_without_input.dart index 5a72860..74ed69a 100644 --- a/lib/src/behaviour_without_input.dart +++ b/lib/src/behaviour_without_input.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:behaviour/behaviour.dart'; /// A [BehaviourWithoutInput] is a type which only has one function, it's @@ -15,16 +17,19 @@ abstract class BehaviourWithoutInput extends BehaviourBase implements BehaviourWithoutInputInterface { /// Super does not need to be called by it's implementers. It only sets the /// [monitor] by which the behaviour can be monitored. - BehaviourWithoutInput({ - BehaviourMonitor? monitor, - }) : super(monitor: monitor); + BehaviourWithoutInput({super.monitor}); /// [call] executes the action of the behaviour. If the action is successful, /// the return value is wrapped in a [Success] else the exception is wrapped /// in a [Failed]. @override - Future> call() { - return executeAction((track) async => Success(await action(track))); + FutureOr> call() { + return executeAction((track) { + return action(track).whenFutureOrValue( + (future) => future.then((result) => Success(result)), + (result) => Success(result), + ); + }); } /// [action] contains the actual logic of the behaviour. @@ -33,5 +38,5 @@ abstract class BehaviourWithoutInput extends BehaviourBase /// [BehaviourTrack.end], [BehaviourTrack.stopWithException] and /// [BehaviourTrack.stopWithError] are called from the super class. @override - Future action(BehaviourTrack? track); + FutureOr action(BehaviourTrack? track); } diff --git a/lib/src/behaviour_without_input_interface.dart b/lib/src/behaviour_without_input_interface.dart index ed7d2d4..d96828d 100644 --- a/lib/src/behaviour_without_input_interface.dart +++ b/lib/src/behaviour_without_input_interface.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:behaviour/behaviour.dart'; /// A [BehaviourWithoutInputInterface] is a type which only has one function, @@ -15,7 +17,7 @@ abstract class BehaviourWithoutInputInterface { /// [call] executes the action of the behaviour. If the action is successful, /// the return value is wrapped in a [Success] else the exception is wrapped /// in a [Failed]. - Future> call(); + FutureOr> call(); /// [action] contains the actual logic of the behaviour. /// @@ -25,5 +27,5 @@ abstract class BehaviourWithoutInputInterface { /// [track] can be used for monitoring. The [BehaviourTrack.start], /// [BehaviourTrack.end], [BehaviourTrack.stopWithException] and /// [BehaviourTrack.stopWithError] are called from the super class. - Future action(BehaviourTrack? track); + FutureOr action(BehaviourTrack? track); } diff --git a/lib/src/exception_or.dart b/lib/src/exception_or.dart index cea31fd..7fa2aaf 100644 --- a/lib/src/exception_or.dart +++ b/lib/src/exception_or.dart @@ -1,42 +1,28 @@ -import 'dart:async'; - /// Abstract class which implementers are either a [Failed] or a [Success]. /// /// Which one of the two implementers the actual instance is, can be found using /// the [when] method. [TSuccess] is the type of the value if the implementation /// is [Success]. The [Failed] also has this type parameter but can be ignored. -abstract class ExceptionOr { +sealed class ExceptionOr { /// Private constructor which prevents other implementations than the ones in /// this file. - const ExceptionOr._(); - - /// Executes [ifFailed] if the implementation is [Failed], executes - /// [ifSuccess] if the implementation is [Success]. - TResult when( - TResult Function(Exception exception) ifFailed, - TResult Function(TSuccess value) ifSuccess, - ); + const ExceptionOr._(this._value); - ExceptionOr whenSuccess( - void Function(TSuccess value) ifSuccess, - ) { - when((exception) {}, ifSuccess); - return this; - } + final dynamic _value; - ExceptionOr whenFailed( - void Function(Exception exception) ifFailed, - ) { - when(ifFailed, (value) {}); - return this; + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (runtimeType != other.runtimeType || other is! ExceptionOr) { + return false; + } + return _value == other._value; } - /// Executes [next] only if the current implementation is [Success]. - ExceptionOr startNextWhenSuccess( - ExceptionOr Function(TSuccess value) next, - ) { - return when((exception) => Failed(exception), next); - } + @override + int get hashCode => _value.hashCode ^ runtimeType.hashCode; } /// Indicates a behaviour failed. @@ -46,27 +32,10 @@ class Failed extends ExceptionOr { /// Indicates a behaviour failed. /// /// [reason] is the reason why the behaviour failed. - const Failed(this.reason) : super._(); + const Failed(this.reason) : super._(reason); /// The reason why the behaviour failed. final Exception reason; - - /// [ifFailed] is executed since this is a [Failed]. - /// - /// [ifSuccess] can be ignored. - @override - TResult when( - TResult Function(Exception exception) ifFailed, - TResult Function(TSuccess value) ifSuccess, - ) { - return ifFailed(reason); - } - - @override - bool operator ==(Object other) => other is Failed && other.reason == reason; - - @override - int get hashCode => reason.hashCode ^ runtimeType.hashCode; } /// Indicates a behaviour executed successfully. @@ -76,57 +45,8 @@ class Success extends ExceptionOr { /// Indicates a behaviour executed successfully. /// /// [value] is the value which the behaviour returned. - const Success(this.value) : super._(); + const Success(this.value) : super._(value); /// The value which the behaviour returned. final TSuccess value; - - /// [ifSuccess] is executed since this is a [Success]. - /// - /// [ifFailed] can be ignored. - @override - TResult when( - TResult Function(Exception exception) ifException, - TResult Function(TSuccess value) ifSuccess, - ) { - return ifSuccess(value); - } - - @override - bool operator ==(Object other) => other is Success && other.value == value; - - @override - int get hashCode => value.hashCode ^ runtimeType.hashCode; -} - -extension FutureExceptionOrExtensions on Future> { - /// Executes [ifFailed] if the future results in a [Failed], executes - /// [ifSuccess] if the future results in a[Success]. - Future thenWhen( - FutureOr Function(Exception exception) ifFailed, - FutureOr Function(T value) ifSuccess, - ) { - return then((value) => value.when(ifFailed, ifSuccess)); - } - - /// Executes [ifSuccess] if the future results in a[Success]. - Future> thenWhenSuccess( - FutureOr Function(T value) ifSuccess, - ) { - return then((value) => value.whenSuccess(ifSuccess)); - } - - /// Executes [ifFailed] if the future results in a [Failed]. - Future> thenWhenFailed( - FutureOr Function(Exception exception) ifFailed, - ) { - return then((value) => value.whenFailed(ifFailed)); - } - - /// Executes [next] only if the current future results in a [Success]. - Future> thenStartNextWhenSuccess( - Future> Function(T value) next, - ) { - return thenWhen((exception) => Failed(exception), next); - } } diff --git a/lib/src/exception_or_extensions.dart b/lib/src/exception_or_extensions.dart new file mode 100644 index 0000000..147d6d0 --- /dev/null +++ b/lib/src/exception_or_extensions.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:behaviour/behaviour.dart'; + +extension ExceptionOrExtensions on ExceptionOr { + /// Executes [ifFailed] if the implementation is [Failed], executes + /// [ifSuccess] if the implementation is [Success]. + TResult when( + TResult Function(Exception exception) ifFailed, + TResult Function(TSuccess value) ifSuccess, + ) { + return switch (this) { + Failed(reason: final reason) => ifFailed(reason), + Success(value: final value) => ifSuccess(value), + }; + } + + /// Executes [next] only if the current implementation is [Success]. + ExceptionOr startNextWhenSuccess( + ExceptionOr Function(TSuccess value) next, + ) { + return switch (this) { + Failed(reason: final reason) => Failed(reason), + Success(value: final value) => next(value), + }; + } +} + +extension FutureExceptionOrExtensions on Future> { + /// Executes [ifFailed] if the future results in a [Failed], executes + /// [ifSuccess] if the future results in a[Success]. + Future thenWhen( + FutureOr Function(Exception exception) ifFailed, + FutureOr Function(T value) ifSuccess, + ) { + return then((value) { + return switch (value) { + Failed(reason: final reason) => ifFailed(reason), + Success(value: final value) => ifSuccess(value), + }; + }); + } + + /// Executes [next] only if the current future results in a [Success]. + Future> thenStartNextWhenSuccess( + FutureOr> Function(T value) next, + ) { + return thenWhen((exception) => Failed(exception), next); + } +} + +extension FutureOrExceptionOrExtensions on FutureOr> { + /// Executes [ifFailed] if the future results in a [Failed], executes + /// [ifSuccess] if the future results in a[Success]. + FutureOr thenWhen( + FutureOr Function(Exception exception) ifFailed, + FutureOr Function(T value) ifSuccess, + ) { + return whenFutureOrValue( + (future) => future.thenWhen(ifFailed, ifSuccess), + (value) => value.when(ifFailed, ifSuccess), + ); + } + + /// Executes [next] only if the current future results in a [Success]. + FutureOr> + thenStartNextWhenSuccess( + FutureOr> Function(T value) next, + ) { + return thenWhen((exception) => Failed(exception), next); + } +} diff --git a/lib/src/future_or_extensions.dart b/lib/src/future_or_extensions.dart new file mode 100644 index 0000000..bd86ce7 --- /dev/null +++ b/lib/src/future_or_extensions.dart @@ -0,0 +1,13 @@ +import 'dart:async'; + +extension FutureOrExtensions on FutureOr { + FutureOr whenFutureOrValue( + FutureOr Function(Future future) future, + FutureOr Function(T value) value, + ) { + return switch (this) { + Future() => future(this as Future), + T() => value(this as T), + }; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b276276..f93d04f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: behaviour description: This package adds support for behaviours. Behaviours are classes of which the instances are used as functions. -version: 2.1.1 +version: 3.0.0 repository: https://github.com/wim07101993/behaviour environment: - sdk: '>=2.14.4 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: async: ">=2.0.0 <3.0.0" @@ -14,5 +14,5 @@ dependencies: dev_dependencies: faker: ">=2.0.0 <3.0.0" lint: - mocktail: ">0.0.0 <1.0.0" + mocktail: ">=1.0.0 <2.0.0" test: ">=1.0.0 <2.0.0" diff --git a/test/src/behaviour_mixin_test.dart b/test/src/behaviour_mixin_test.dart index f955e09..8f4d21b 100644 --- a/test/src/behaviour_mixin_test.dart +++ b/test/src/behaviour_mixin_test.dart @@ -17,7 +17,7 @@ void main() { setUpAll(() { registerFallbackValue( _BehaviourMixinImpl( - onCatchException: (e, stacktrace, track) => Exception(), + onCatchException: (e, stackTrace, track) => Exception(), ), ); registerFallbackValue(Exception()); @@ -112,6 +112,21 @@ void main() { // act final result = await behaviour.executeAction((track) => expected); + // assert + expect(identical(result, expected), isTrue); + }); + + test('should execute async and return result of action', () async { + // arrange + final expected = faker.randomGenerator.element>([ + Failed(Exception()), + Success(faker.lorem.word()), + ]); + + // act + final result = + await behaviour.executeAction((track) => Future.value(expected)); + // assert expect(result, expected); }); @@ -125,6 +140,25 @@ void main() { // act final result = await behaviour.executeAction((track) => throw exception); + // assert + expect(result, isA()); + final resultException = (result as Failed).reason; + expect(identical(resultException, exception), isTrue); + verify(() => mockTrack.stopWithException(exception, any())); + }); + + test( + 'should stop track with exception and return failed if an exception happens in async action', + () async { + // arrange + final exception = Exception(); + + // act + final result = await behaviour.executeAction((track) async { + await Future.delayed(const Duration(milliseconds: 1)); + throw exception; + }); + // assert expect(result, isA()); result.when( @@ -169,6 +203,29 @@ void main() { (value) => throw AssertionError('The result should be a failed.'), ); }); + + test( + 'should return failed if an unknown error happens in the action using the async catch', + () async { + // arrange + final error = faker.lorem.sentence(); + final mockOnCatch = _MockOnCatch(); + final exception = Exception(); + when(() => mockOnCatch.onCatch(any(), any(), any())).thenAnswer((_) { + return Future.value(exception); + }); + behaviour = _BehaviourMixinImpl(onCatchError: mockOnCatch.onCatch); + + // act + final result = await behaviour.executeAction((track) => throw error); + + // assert + expect(result, isA()); + result.when( + (e) => expect(e, exception), + (value) => throw AssertionError('The result should be a failed.'), + ); + }); }); group('onCatchException', () { diff --git a/test/src/behaviour_mixin_test.types.dart b/test/src/behaviour_mixin_test.types.dart index 6ba53ed..620a4a2 100644 --- a/test/src/behaviour_mixin_test.types.dart +++ b/test/src/behaviour_mixin_test.types.dart @@ -9,16 +9,14 @@ class _BehaviourMixinImpl with BehaviourMixin { this.monitor, FutureOr Function( Exception e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, - )? - onCatchException, + )? onCatchException, FutureOr Function( Object e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, - )? - onCatchError, + )? onCatchError, }) : _onCatchException = onCatchException, _onCatchError = onCatchError; @@ -34,32 +32,32 @@ class _BehaviourMixinImpl with BehaviourMixin { @override FutureOr onCatchException( Exception e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { final fakeCatch = _onCatchException; return fakeCatch != null - ? fakeCatch(e, stacktrace, track) - : super.onCatchException(e, stacktrace, track); + ? fakeCatch(e, stackTrace, track) + : super.onCatchException(e, stackTrace, track); } @override FutureOr onCatchError( Object e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { final fakeCatch = _onCatchError; return fakeCatch != null - ? fakeCatch(e, stacktrace, track) - : super.onCatchError(e, stacktrace, track); + ? fakeCatch(e, stackTrace, track) + : super.onCatchError(e, stackTrace, track); } } class _OnCatch { FutureOr onCatch( Object e, - StackTrace stacktrace, + StackTrace stackTrace, BehaviourTrack? track, ) { throw UnimplementedError(); diff --git a/test/src/behaviour_test.dart b/test/src/behaviour_test.dart index 5025514..e5e57e1 100644 --- a/test/src/behaviour_test.dart +++ b/test/src/behaviour_test.dart @@ -84,7 +84,22 @@ void main() { final result = await behaviour(faker.lorem.sentence()); // assert - expect(result, Success(value)); + expect(result, Success(value)); + }); + + test('should make use of the [FutureOr] functionality when success', + () async { + // arrange + final value = _DummyType(1); + when(() => mockAction.action(any(), any())).thenAnswer((i) => value); + + // act + final result = behaviour(faker.lorem.sentence()); + + // assert + expect(result, isA()); + final resultValue = (result as Success).value; + expect(identical(resultValue, value), isTrue); }); }); } diff --git a/test/src/behaviour_test.types.dart b/test/src/behaviour_test.types.dart index e96eb5e..0b373c2 100644 --- a/test/src/behaviour_test.types.dart +++ b/test/src/behaviour_test.types.dart @@ -4,16 +4,15 @@ class _MockAction extends Mock implements _Action {} class _BehaviourImpl extends Behaviour { _BehaviourImpl({ - required Future Function(TIn input, BehaviourTrack? track) action, + required FutureOr Function(TIn input, BehaviourTrack? track) action, required this.description, - BehaviourMonitor? monitor, - }) : _action = action, - super(monitor: monitor); + super.monitor, + }) : _action = action; - final Future Function(TIn input, BehaviourTrack? track) _action; + final FutureOr Function(TIn input, BehaviourTrack? track) _action; @override - Future action(TIn input, BehaviourTrack? track) { + FutureOr action(TIn input, BehaviourTrack? track) { return _action(input, track); } @@ -22,7 +21,13 @@ class _BehaviourImpl extends Behaviour { } class _Action { - Future action(TIn input, BehaviourTrack? track) { + FutureOr action(TIn input, BehaviourTrack? track) { throw UnimplementedError(); } } + +class _DummyType { + _DummyType(this.value); + + final int value; +} diff --git a/test/src/behaviour_track_test.types.dart b/test/src/behaviour_track_test.types.dart index 75bcbdb..606f38b 100644 --- a/test/src/behaviour_track_test.types.dart +++ b/test/src/behaviour_track_test.types.dart @@ -1,7 +1,7 @@ part of 'behaviour_track_test.dart'; class _BehaviourTrackImpl extends BehaviourTrack { - const _BehaviourTrackImpl(BehaviourMixin behaviour) : super(behaviour); + const _BehaviourTrackImpl(super.behaviour); @override void addAttribute(String key, Object value) {} diff --git a/test/src/behaviour_without_input_test.dart b/test/src/behaviour_without_input_test.dart index 58d3b47..cab66c5 100644 --- a/test/src/behaviour_without_input_test.dart +++ b/test/src/behaviour_without_input_test.dart @@ -83,7 +83,22 @@ void main() { final result = await behaviour(); // assert - expect(result, Success(value)); + expect(result, Success(value)); + }); + + test('should make use of the [FutureOr] functionality when success', + () async { + // arrange + final value = _DummyType(1); + when(() => mockAction.action(any())).thenAnswer((i) => value); + + // act + final result = behaviour(); + + // assert + expect(result, isA()); + final resultValue = (result as Success).value; + expect(identical(resultValue, value), isTrue); }); }); } diff --git a/test/src/behaviour_without_input_test.types.dart b/test/src/behaviour_without_input_test.types.dart index 6df8044..b6e7d67 100644 --- a/test/src/behaviour_without_input_test.types.dart +++ b/test/src/behaviour_without_input_test.types.dart @@ -4,16 +4,15 @@ class _MockAction extends Mock implements _Action {} class _BehaviourWithoutInputImpl extends BehaviourWithoutInput { _BehaviourWithoutInputImpl({ - required Future Function(BehaviourTrack? track) action, + required FutureOr Function(BehaviourTrack? track) action, required this.description, - BehaviourMonitor? monitor, - }) : _action = action, - super(monitor: monitor); + super.monitor, + }) : _action = action; - final Future Function(BehaviourTrack? track) _action; + final FutureOr Function(BehaviourTrack? track) _action; @override - Future action(BehaviourTrack? track) { + FutureOr action(BehaviourTrack? track) { return _action(track); } @@ -22,7 +21,13 @@ class _BehaviourWithoutInputImpl extends BehaviourWithoutInput { } class _Action { - Future action(BehaviourTrack? track) { + FutureOr action(BehaviourTrack? track) { throw UnimplementedError(); } } + +class _DummyType { + _DummyType(this.value); + + final int value; +} diff --git a/test/src/exception_or_extensions_test.dart b/test/src/exception_or_extensions_test.dart new file mode 100644 index 0000000..4aeff76 --- /dev/null +++ b/test/src/exception_or_extensions_test.dart @@ -0,0 +1,214 @@ +import 'dart:async'; + +import 'package:behaviour/behaviour.dart'; +import 'package:faker/faker.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +void main() { + setUpAll(() { + registerFallbackValue(Exception()); + }); + + group('ExceptionOrExtensions', () { + group('when', () { + test('should invoke the ifException when [Failed]', () { + // act + final exception = Exception(faker.lorem.sentence()); + final failed = Failed(exception); + + // assert + failed.when( + (e) => expect(e, exception), + (value) => throw AssertionError('Should have invoked success.'), + ); + }); + + test('should invoke the ifSuccess when [Success]', () { + // act + final value = faker.lorem.sentence(); + final success = Success(value); + + // assert + success.when( + (exception) => throw AssertionError('Should have invoked exception.'), + (v) => expect(v, value), + ); + }); + }); + + group('startNextWhenSuccess', () { + test('should return first failure if not success', () { + // arrange + final failed = Failed(Exception(faker.lorem.sentence())); + + // act + final result = failed.startNextWhenSuccess((value) { + return Failed(Exception(faker.lorem.word())); + }); + + // assert + expect(result, failed); + }); + + test('should invoke next if current results to success', () { + // arrange + final success = Success(faker.lorem.sentence()); + final secondSuccess = Success(faker.lorem.word()); + + // act + final result = success.startNextWhenSuccess((value) { + expect(value, success.value); + return secondSuccess; + }); + + // assert + expect(result, secondSuccess); + }); + }); + }); + + group('FutureExceptionOrExtensions', () { + group('thenWhen', () { + test('should invoke the ifException when [Failed]', () async { + // act + final exception = Exception(faker.lorem.sentence()); + final failed = Failed(exception); + final future = Future.value(failed); + + // assert + await future.thenWhen( + (e) => expect(e, exception), + (value) => throw AssertionError('Should have invoked success.'), + ); + }); + + test('should invoke the ifSuccess when [Success]', () async { + // act + final value = faker.lorem.sentence(); + final success = Success(value); + final future = Future.value(success); + + // assert + await future.thenWhen( + (exception) => throw AssertionError('Should have invoked exception.'), + (v) => expect(v, value), + ); + }); + }); + + group('startNextWhenSuccess', () { + test('should return first failure if not success', () async { + // arrange + final failed = Failed(Exception(faker.lorem.sentence())); + final future = Future.value(failed); + + // act + final result = await future.thenStartNextWhenSuccess((value) { + return Failed(Exception(faker.lorem.word())); + }); + + // assert + expect(result, failed); + }); + + test('should invoke next if current results to success', () async { + // arrange + final success = Success(faker.lorem.sentence()); + final future = Future.value(success); + final secondSuccess = Success(faker.lorem.word()); + + // act + final result = await future.thenStartNextWhenSuccess((value) { + expect(value, success.value); + return secondSuccess; + }); + + // assert + expect(result, secondSuccess); + }); + }); + }); + + group('FutureOrExceptionOrExtensions', () { + group('thenWhen', () { + test('should invoke the ifException when [Failed]', () async { + // act + final exception = Exception(faker.lorem.sentence()); + final failed = Failed(exception); + + // assert + await failed.thenWhen( + (e) => expect(e, exception), + (value) => throw AssertionError('Should have invoked success.'), + ); + }); + + test('should invoke the ifSuccess when [Success]', () async { + // act + final value = faker.lorem.sentence(); + final success = Success(value); + + // assert + await success.thenWhen( + (exception) => throw AssertionError('Should have invoked exception.'), + (v) => expect(v, value), + ); + }); + + test('should invoke the ifException when [Failed future]', () async { + // act + final exception = Exception(faker.lorem.sentence()); + final FutureOr failed = Future.value(Failed(exception)); + + // assert + await failed.thenWhen( + (e) => expect(e, exception), + (value) => throw AssertionError('Should have invoked success.'), + ); + }); + + test('should invoke the ifSuccess when [Success future]', () async { + // act + final value = faker.lorem.sentence(); + final FutureOr success = Future.value(Success(value)); + + // assert + await success.thenWhen( + (exception) => throw AssertionError('Should have invoked exception.'), + (v) => expect(v, value), + ); + }); + }); + + group('startNextWhenSuccess', () { + test('should return first failure if not success', () async { + // arrange + final failed = Failed(Exception(faker.lorem.sentence())); + + // act + final result = await failed.thenStartNextWhenSuccess((value) { + return Failed(Exception(faker.lorem.word())); + }); + + // assert + expect(result, failed); + }); + + test('should invoke next if current results to success', () async { + // arrange + final success = Success(faker.lorem.sentence()); + final secondSuccess = Success(faker.lorem.word()); + + // act + final result = await success.thenStartNextWhenSuccess((value) { + expect(value, success.value); + return secondSuccess; + }); + + // assert + expect(result, secondSuccess); + }); + }); + }); +} diff --git a/test/src/exception_or_test.dart b/test/src/exception_or_test.dart index 37760b9..534e84e 100644 --- a/test/src/exception_or_test.dart +++ b/test/src/exception_or_test.dart @@ -3,14 +3,6 @@ import 'package:faker/faker.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; -abstract class _FunctionWrapper { - void call(TInput input); -} - -class MockFunction extends Mock implements _FunctionWrapper {} - -class MockExceptionOr extends Mock implements ExceptionOr {} - void main() { setUpAll(() { registerFallbackValue(Exception()); @@ -49,292 +41,111 @@ void main() { }); }); - group('whenSuccess', () { - test('should execute ifSuccess when the exceptionOr is a success', () { - // arrange - final fakeValue = faker.lorem.sentence(); - final function = MockFunction(); - final ExceptionOr exceptionOr = Success(fakeValue); - + group('operator ==', () { + test('should return true if the values are equals (Success)', () { // act - exceptionOr.whenSuccess(function); + final value = faker.lorem.sentence(); // assert - verify(() => function.call(fakeValue)); + expect(Success(value) == Success(value), isTrue); }); - test('should not execute ifSuccess when the exceptionOr is a failed', () { - // arrange - final function = MockFunction(); - final ExceptionOr exceptionOr = Failed( - Exception(faker.lorem.sentence()), - ); - + test('should return true if the values are equals (Failed)', () { // act - exceptionOr.whenSuccess(function); + final exception = Exception(faker.lorem.sentence()); // assert - verifyNever(() => function.call(any())); + expect(Failed(exception) == Failed(exception), isTrue); }); - }); - group('whenFailed', () { - test('should execute ifFailed when the exceptionOr is a failed', () { + test('should return false if the values are different (Success)', () { // arrange - final fakeException = Exception(faker.lorem.sentence()); - final function = MockFunction(); - final ExceptionOr exceptionOr = Failed(fakeException); - - // act - exceptionOr.whenFailed(function); + final first = Success(faker.lorem.word()); + final second = Success(faker.lorem.sentence()); // assert - verify(() => function.call(fakeException)); + expect(first == second, isFalse); }); - test('should not execute ifFailed when the exceptionOr is a success', () { + test('should return false if the values are different (Failed)', () { // arrange - final function = MockFunction(); - final ExceptionOr exceptionOr = Success(faker.lorem.sentence()); - - // act - exceptionOr.whenFailed(function); + final first = Failed(Exception(faker.lorem.sentence())); + final second = Failed(Exception(faker.lorem.word())); // assert - verifyNever(() => function.call(any())); + expect(first == second, isFalse); }); }); - }); - - group(Failed, () { - late Exception exception; - - setUp(() { - exception = Exception(faker.lorem.sentence()); - }); - group('constructor', () { - test('should set the fields', () { - // act - final failed = Failed(exception); + group('hashCode', () { + test('should be the same for the same values (Success)', () { + // arrange + final value = faker.lorem.sentence(); // assert - expect(failed.reason, exception); + expect(Success(value).hashCode, Success(value).hashCode); }); - }); - group('when', () { - test('should invoke the ifException', () { - // act - final failed = Failed(exception); - - // assert - failed.when( - (e) => expect(e, exception), - (value) => throw AssertionError('Should have invoked success.'), - ); - }); - }); + test('should be the same for the same values (Failed)', () { + // arrange + final exception = Exception(faker.lorem.sentence()); - group('operator ==', () { - test('should return true if the values are equals', () { // assert - expect(Failed(exception) == Failed(exception), isTrue); + expect(Failed(exception).hashCode, Failed(exception).hashCode); }); - test('should return false if the values are different', () { + test('should be different for different values (Success)', () { // arrange - final other = Failed(Exception(faker.lorem.word())); + final first = Success(faker.lorem.sentence()); + final second = Success(faker.lorem.word()); // assert - expect(Failed(exception) == other, isFalse); + expect(first.hashCode, isNot(second.hashCode)); }); - }); - group('hashCode', () { - test('should be the same for the same values', () { - // assert - expect(Failed(exception).hashCode, Failed(exception).hashCode); - }); + test('should be different for different values (Failed)', () { + // arrange + final first = Failed(Exception(faker.lorem.sentence())); + final second = Failed(Exception(faker.lorem.word())); - test('should be different for different values', () { // assert - final other = Failed(Exception(faker.lorem.word())); - - expect(Failed(exception).hashCode, isNot(other.hashCode)); + expect(first.hashCode, isNot(second.hashCode)); }); }); }); - group(Success, () { - late String value; + group(Failed, () { + late Exception exception; setUp(() { - value = faker.lorem.sentence(); + exception = Exception(faker.lorem.sentence()); }); group('constructor', () { test('should set the fields', () { // act - final success = Success(value); - - // assert - expect(success.value, value); - }); - }); - - group('when', () { - test('should invoke the ifSuccess', () { - // act - final success = Success(value); - - // assert - success.when( - (exception) => throw AssertionError('Should have invoked exception.'), - (v) => expect(v, value), - ); - }); - }); - - group('operator ==', () { - test('should return true if the values are equals', () { - // assert - expect(Success(value) == Success(value), isTrue); - }); - - test('should return false if the values are different', () { - // assert - expect(Success(value) == Success(faker.lorem.word()), isFalse); - }); - }); - - group('hashCode', () { - test('should be the same for the same values', () { - // assert - expect(Success(value).hashCode, Success(value).hashCode); - }); + final failed = Failed(exception); - test('should be different for different values', () { // assert - final other = Success(faker.lorem.word()); - - expect(Success(value).hashCode, isNot(other.hashCode)); + expect(failed.reason, exception); }); }); }); - group('FutureExceptionOrExtensions', () { - late Exception fakeException; - late ExceptionOr mockExceptionOr; + group(Success, () { + late String value; setUp(() { - fakeException = Exception(faker.lorem.sentence()); - mockExceptionOr = MockExceptionOr(); - - when(() => mockExceptionOr.whenFailed(any())).thenReturn(mockExceptionOr); - when(() => mockExceptionOr.whenSuccess(any())) - .thenReturn(mockExceptionOr); - when(() => mockExceptionOr.when(any(), any())).thenAnswer((i) { - // ignore: avoid_dynamic_calls - return i.positionalArguments[0](fakeException); - }); + value = faker.lorem.sentence(); }); - group('thenWhen', () { - test('should invoke the ifException when failed', () async { - // arrange - final exception = Exception(faker.lorem.sentence()); - final failed = Failed(exception); - final future = Future.value(failed); - final fakeReturnValue = faker.lorem.sentence(); - - // act - final returnValue = await future.thenWhen( - (e) => fakeReturnValue, - (value) => throw AssertionError('Should have invoked success.'), - ); - - // assert - expect(returnValue, fakeReturnValue); - }); - - test('should invoke the ifSuccess when success', () async { + group('constructor', () { + test('should set the fields', () { // act - final value = faker.lorem.sentence(); final success = Success(value); - final future = Future.value(success); - final fakeReturnValue = faker.lorem.sentence(); - - // act - final returnValue = await future.thenWhen( - (exception) => throw AssertionError('Should have invoked exception.'), - (v) => fakeReturnValue, - ); - - // assert - expect(returnValue, fakeReturnValue); - }); - }); - - group('thenWhenSuccess', () { - test('should invoke the whenSuccess of the exceptionOr', () async { - // arrange - final future = Future.value(mockExceptionOr); - String successHandler(String value) => faker.lorem.sentence(); - - // act - final returnValue = await future.thenWhenSuccess(successHandler); // assert - expect(returnValue, mockExceptionOr); - verify(() => mockExceptionOr.whenSuccess(successHandler)); - }); - }); - - group('thenWhenFailed', () { - test('should invoke the whenFailed of the exceptionOr', () async { - // arrange - final future = Future.value(mockExceptionOr); - String exceptionHandler(Exception value) => faker.lorem.sentence(); - - // act - final returnValue = await future.thenWhenFailed(exceptionHandler); - - // assert - expect(returnValue, mockExceptionOr); - verify(() => mockExceptionOr.whenFailed(exceptionHandler)); - }); - }); - - group('thenStartNextWhenSuccess', () { - test('should return first failure if not success', () async { - // arrange - final failed = Failed(Exception(faker.lorem.sentence())); - final future = Future.value(failed); - - // act - final result = await future.thenStartNextWhenSuccess((value) { - return Future.value(Failed(Exception(faker.lorem.word()))); - }); - - // assert - expect(result, failed); - }); - - test('should invoke next if current results to success', () async { - // arrange - final success = Success(faker.lorem.sentence()); - final future = Future.value(success); - final secondSuccess = Success(faker.lorem.word()); - - // act - final result = await future.thenStartNextWhenSuccess((value) { - expect(value, success.value); - return Future.value(secondSuccess); - }); - - // assert - expect(result, secondSuccess); + expect(success.value, value); }); }); }); diff --git a/test/src/future_or_extensions_test.dart b/test/src/future_or_extensions_test.dart new file mode 100644 index 0000000..970d734 --- /dev/null +++ b/test/src/future_or_extensions_test.dart @@ -0,0 +1,33 @@ +import 'package:behaviour/behaviour.dart'; +import 'package:faker/faker.dart'; +import 'package:test/test.dart'; + +void main() { + group('whenFutureOrValue', () { + test('should execute value when value', () async { + // arrange + final expected = faker.lorem.sentence(); + + // assert + await expected.whenFutureOrValue( + (future) => throw AssertionError( + 'This is no future, value should have been executed', + ), + (value) => expect(value == expected, isTrue), + ); + }); + + test('should execute future when future', () async { + // arrange + final expected = faker.lorem.sentence(); + + // assert + await Future.value(expected).whenFutureOrValue( + (future) => future.then((value) => expect(value == expected, isTrue)), + (value) => throw AssertionError( + 'This is no future, value should have been executed', + ), + ); + }); + }); +}