Skip to content

Commit

Permalink
Add blocPresentationTest function
Browse files Browse the repository at this point in the history
  • Loading branch information
KrzysztofMamak authored Oct 4, 2023
2 parents 2ccddfb + 2ced114 commit 1b7a478
Show file tree
Hide file tree
Showing 17 changed files with 913 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/bloc_presentation_test-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- name: Run analyzer
run: flutter analyze --fatal-warnings --fatal-infos

- name: Run tests
run: flutter test

- name: Dry run pub publish
uses: leancodepl/mobile-tools/.github/actions/pub-release@pub-release-v1
with:
Expand Down
6 changes: 5 additions & 1 deletion packages/bloc_presentation_test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.0.0-pre.1

- Add `blocPresentationTest` function

# 0.3.0

- Bump dependency on `bloc_presentation` to `^0.4.0` (#25)
Expand All @@ -14,4 +18,4 @@

# 0.1.0

- Initial release. Includes `MockPresentationBloc` / `MockPresentationCubit` and `whenListenPresentation`.
- Initial release. Includes `MockPresentationBloc`/`MockPresentationCubit` and `whenListenPresentation`.
85 changes: 78 additions & 7 deletions packages/bloc_presentation_test/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,40 @@ import 'package:test/test.dart';
class CounterCubit extends Cubit<int>
with BlocPresentationMixin<int, CounterCubitEvent> {
CounterCubit() : super(0);

void count() {
final newNumber = state + 1;

if (state < 10) {
emitPresentation(CounterPresentationEvent(newNumber));
}

emit(newNumber);
}
}

sealed class CounterCubitEvent {}

final class CounterPresentationEvent implements CounterCubitEvent {
const CounterPresentationEvent();
const CounterPresentationEvent(this.number);

final int number;

@override
int get hashCode => number;

@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}

if (other.runtimeType != runtimeType) {
return false;
}

return other is CounterPresentationEvent && other.number == number;
}
}

class MockCounterPresentationCubit
Expand All @@ -26,6 +54,7 @@ class MockCounterCubit extends MockCubit<int> implements CounterCubit {}
void main() {
mainMockPresentationCubit();
mainWhenListenPresentation();
mainBlocPresentationTest();
}

void mainMockPresentationCubit() {
Expand All @@ -42,13 +71,13 @@ void mainMockPresentationCubit() {
test(
'presentation stream emits events properly',
() async {
mockCubit.emitMockPresentation(const CounterPresentationEvent());
mockCubit.emitMockPresentation(const CounterPresentationEvent(5));

await expectLater(
mockCubit.presentation,
emitsInOrder(
<Matcher>[
equals(const CounterPresentationEvent()),
equals(const CounterPresentationEvent(5)),
],
),
);
Expand All @@ -64,7 +93,7 @@ void mainWhenListenPresentation() {
mockCubit = MockCounterCubit();
controller = whenListenPresentation(
mockCubit,
initialEvents: [const CounterPresentationEvent()],
initialEvents: [const CounterPresentationEvent(1)],
);
});

Expand All @@ -75,17 +104,59 @@ void mainWhenListenPresentation() {
test(
'presentation stream emits events properly',
() async {
controller.add(const CounterPresentationEvent());
controller.add(const CounterPresentationEvent(2));

await expectLater(
mockCubit.presentation,
emitsInOrder(
<Matcher>[
equals(const CounterPresentationEvent()),
equals(const CounterPresentationEvent()),
equals(const CounterPresentationEvent(1)),
equals(const CounterPresentationEvent(2)),
],
),
);
},
);
}

void mainBlocPresentationTest() {
CounterCubit buildCubit() => CounterCubit();

blocPresentationTest<CounterCubit, int, CounterCubitEvent>(
'emits correct presentation event after calling count when state was '
'smaller than 10',
build: buildCubit,
act: (cubit) => cubit.count(),
expectPresentation: () => const [
CounterPresentationEvent(1),
],
);

blocPresentationTest<CounterCubit, int, CounterCubitEvent>(
'does not emit presentation event if count has not ben called',
build: buildCubit,
expectPresentation: () => const <CounterPresentationEvent>[],
);

blocPresentationTest<CounterCubit, int, CounterCubitEvent>(
'emits correct presentation event after calling count 2 times when state '
'was smaller than 10',
build: buildCubit,
act: (cubit) => cubit
..count()
..count(),
// skipPresentation can be used for skipping events during verification
skipPresentation: 1,
expectPresentation: () => const [
CounterPresentationEvent(2),
],
);

blocPresentationTest<CounterCubit, int, CounterCubitEvent>(
'does not emit presentation event after calling count when state was 10',
build: buildCubit,
seed: () => 10,
act: (cubit) => cubit.count(),
expectPresentation: () => const <CounterPresentationEvent>[],
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// A testing library for bloc_presentation.
library;

export 'src/bloc_presentation_test.dart';
export 'src/mock_presentation_bloc.dart';
export 'src/when_listen_presentation.dart';
180 changes: 180 additions & 0 deletions packages/bloc_presentation_test/lib/src/bloc_presentation_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import 'dart:async';

import 'package:bloc_presentation/bloc_presentation.dart';
import 'package:diff_match_patch/diff_match_patch.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';

/// This function provides a possibility to test blocs/cubits with
/// [BlocPresentationMixin] mixed in.
///
/// Events emitted via presentation stream can be verified by
/// [expectPresentation].
///
/// This function is not intended to verify states, but
/// only to verify presentation events.
@isTest
void blocPresentationTest<B extends BlocPresentationMixin<State, P>, State, P>(
String description, {
required B Function() build,
FutureOr<void> Function()? setUp,
State Function()? seed,
dynamic Function(B bloc)? act,
Duration? wait,
int skipPresentation = 0,
required dynamic Function() expectPresentation,
FutureOr<void> Function()? tearDown,
dynamic tags,
}) {
test(
description,
() async {
await testBlocPresentation<B, State, P>(
setUp: setUp,
build: build,
seed: seed,
act: act,
wait: wait,
skipPresentation: skipPresentation,
expectPresentation: expectPresentation,
tearDown: tearDown,
);
},
tags: tags,
);
}

/// Internal [blocPresentationTest] runner which is only visible for testing.
/// This should never be used directly -- please use [blocPresentationTest]
/// instead.
@visibleForTesting
Future<void>
testBlocPresentation<B extends BlocPresentationMixin<State, P>, State, P>({
FutureOr<void> Function()? setUp,
required B Function() build,
State Function()? seed,
dynamic Function(B bloc)? act,
Duration? wait,
int skipPresentation = 0,
required dynamic Function() expectPresentation,
FutureOr<void> Function()? tearDown,
dynamic tags,
}) async {
var shallowEquality = false;

try {
await _runZonedGuarded(() async {
await setUp?.call();

final events = <P>[];
final bloc = build();

if (seed != null) {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
bloc.emit(seed());
}

final subscription =
bloc.presentation.skip(skipPresentation).listen(events.add);

await act?.call(bloc);

if (wait != null) {
await Future<void>.delayed(wait);
}

await Future<void>.delayed(Duration.zero);
await bloc.close();

final dynamic expected = expectPresentation();
shallowEquality = '$events' == '$expected';

try {
expect(events, wrapMatcher(expected));
} on TestFailure catch (e) {
if (shallowEquality || expected is! List<P>) {
rethrow;
}

final diff = _diff(expected: expected, actual: events);
final message = '${e.message}\n$diff';

// ignore: only_throw_errors
throw TestFailure(message);
}

await subscription.cancel();
await tearDown?.call();
});
} catch (e) {
if (shallowEquality && e is TestFailure) {
// ignore: only_throw_errors
throw TestFailure(
'''
${e.message}
WARNING: Please ensure presentation events extend Equatable, override == and hashCode, or implement Comparable.
Alternatively, consider using Matchers in the expectPresentation of the blocPresentationTest rather than concrete presentation events instances.\n''',
);
}

rethrow;
}
}

// Based on bloc_test package
Future<void> _runZonedGuarded(Future<void> Function() body) {
final completer = Completer<void>();

runZonedGuarded(() async {
await body();

if (!completer.isCompleted) {
completer.complete();
}
}, (e, st) {
if (!completer.isCompleted) {
completer.completeError(e, st);
}
});

return completer.future;
}

// Based on bloc_test package
String _diff({required dynamic expected, required dynamic actual}) {
final buffer = StringBuffer();
final differences = diff(expected.toString(), actual.toString());

buffer
..writeln('${'=' * 4} diff ${'=' * 40}')
..writeln()
..writeln(differences.toPrettyString())
..writeln()
..writeln('${'=' * 4} end diff ${'=' * 36}');

return buffer.toString();
}

extension on List<Diff> {
// Based on bloc_test package
String toPrettyString() {
String identical(String str) => '\u001b[90m$str\u001B[0m';
String deletion(String str) => '\u001b[31m[-$str-]\u001B[0m';
String insertion(String str) => '\u001b[32m{+$str+}\u001B[0m';

final buffer = StringBuffer();

for (final difference in this) {
switch (difference.operation) {
case DIFF_EQUAL:
buffer.write(identical(difference.text));
case DIFF_DELETE:
buffer.write(deletion(difference.text));
case DIFF_INSERT:
buffer.write(insertion(difference.text));
}
}

return buffer.toString();
}
}
6 changes: 5 additions & 1 deletion packages/bloc_presentation_test/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: bloc_presentation_test
description: A testing library for Blocs/Cubits which mixin BlocPresentationMixin. To be used with bloc_presentation package.
version: 0.3.0
version: 1.0.0-pre.1
homepage: https://github.com/leancodepl/bloc_presentation/tree/master/packages/bloc_presentation_test

environment:
Expand All @@ -10,9 +10,13 @@ environment:
dependencies:
bloc: ^8.0.0
bloc_presentation: ^0.4.0
diff_match_patch: ^0.4.1
flutter:
sdk: flutter
meta: ^1.9.1
mocktail: ^1.0.0
test: ^1.24.3

dev_dependencies:
equatable: ^2.0.5
leancode_lint: '>=5.0.0'
Loading

0 comments on commit 1b7a478

Please sign in to comment.