feat: improved tests

pull/119/head
alestiago 4 years ago
parent 37d583ba6d
commit 30330da306

@ -16,11 +16,10 @@ import 'package:pinball_components/pinball_components.dart';
/// is awarded, and the [BigDashNestBumper] releases a new [Ball]. /// is awarded, and the [BigDashNestBumper] releases a new [Ball].
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from [Component]. // TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from [Component].
class FlutterForest extends Component class FlutterForest extends Component with Controls<_FlutterForestController> {
with Controls<FlutterForestController>, HasGameRef<PinballGame> {
/// {@macro flutter_forest} /// {@macro flutter_forest}
FlutterForest() { FlutterForest() {
controller = FlutterForestController(this); controller = _FlutterForestController(this);
} }
@override @override
@ -28,14 +27,15 @@ class FlutterForest extends Component
await super.onLoad(); await super.onLoad();
final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3);
final bigNest = ControlledBigDashNestBumper(id: 'big_nest_bumper') final bigNest = _ControlledBigDashNestBumper(
..initialPosition = Vector2(18.55, 59.35); id: 'big_nest_bumper',
final smallLeftNest = )..initialPosition = Vector2(18.55, 59.35);
ControlledSmallDashNestBumper.a(id: 'small_nest_bumper_a') final smallLeftNest = _ControlledSmallDashNestBumper.a(
..initialPosition = Vector2(8.95, 51.95); id: 'small_nest_bumper_a',
final smallRightNest = )..initialPosition = Vector2(8.95, 51.95);
ControlledSmallDashNestBumper.b(id: 'small_nest_bumper_b') final smallRightNest = _ControlledSmallDashNestBumper.b(
..initialPosition = Vector2(23.3, 46.75); id: 'small_nest_bumper_b',
)..initialPosition = Vector2(23.3, 46.75);
await addAll([ await addAll([
signPost, signPost,
@ -49,15 +49,15 @@ class FlutterForest extends Component
/// {@template flutter_forest_controller} /// {@template flutter_forest_controller}
/// ///
/// {@endtemplate} /// {@endtemplate}
class FlutterForestController extends ComponentController<FlutterForest> class _FlutterForestController extends ComponentController<FlutterForest>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> { with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro flutter_forest_controller} /// {@macro flutter_forest_controller}
FlutterForestController(FlutterForest flutterForest) : super(flutterForest); _FlutterForestController(FlutterForest flutterForest) : super(flutterForest);
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
gameRef.addContactCallback(ControlledDashNestBumperBallContactCallback()); gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback());
} }
@override @override
@ -81,10 +81,10 @@ class FlutterForestController extends ComponentController<FlutterForest>
/// {@template controlled_big_dash_nest_bumper} /// {@template controlled_big_dash_nest_bumper}
/// A [BigDashNestBumper] controlled by a [DashNestBumperController]. /// A [BigDashNestBumper] controlled by a [DashNestBumperController].
/// {@endtemplate} /// {@endtemplate}
class ControlledBigDashNestBumper extends BigDashNestBumper class _ControlledBigDashNestBumper extends BigDashNestBumper
with Controls<DashNestBumperController>, ScorePoints { with Controls<DashNestBumperController>, ScorePoints {
/// {@macro controlled_big_dash_nest_bumper} /// {@macro controlled_big_dash_nest_bumper}
ControlledBigDashNestBumper({required String id}) : super() { _ControlledBigDashNestBumper({required String id}) : super() {
controller = DashNestBumperController(this, id: id); controller = DashNestBumperController(this, id: id);
} }
@ -95,15 +95,15 @@ class ControlledBigDashNestBumper extends BigDashNestBumper
/// {@template controlled_small_dash_nest_bumper} /// {@template controlled_small_dash_nest_bumper}
/// A [SmallDashNestBumper] controlled by a [DashNestBumperController]. /// A [SmallDashNestBumper] controlled by a [DashNestBumperController].
/// {@endtemplate} /// {@endtemplate}
class ControlledSmallDashNestBumper extends SmallDashNestBumper class _ControlledSmallDashNestBumper extends SmallDashNestBumper
with Controls<DashNestBumperController>, ScorePoints { with Controls<DashNestBumperController>, ScorePoints {
/// {@macro controlled_small_dash_nest_bumper} /// {@macro controlled_small_dash_nest_bumper}
ControlledSmallDashNestBumper.a({required String id}) : super.a() { _ControlledSmallDashNestBumper.a({required String id}) : super.a() {
controller = DashNestBumperController(this, id: id); controller = DashNestBumperController(this, id: id);
} }
/// {@macro controlled_small_dash_nest_bumper} /// {@macro controlled_small_dash_nest_bumper}
ControlledSmallDashNestBumper.b({required String id}) : super.b() { _ControlledSmallDashNestBumper.b({required String id}) : super.b() {
controller = DashNestBumperController(this, id: id); controller = DashNestBumperController(this, id: id);
} }
@ -114,24 +114,24 @@ class ControlledSmallDashNestBumper extends SmallDashNestBumper
/// {@template dash_nest_bumper_controller} /// {@template dash_nest_bumper_controller}
/// Controls a [DashNestBumper]. /// Controls a [DashNestBumper].
/// {@endtemplate} /// {@endtemplate}
@visibleForTesting
class DashNestBumperController extends ComponentController<DashNestBumper> class DashNestBumperController extends ComponentController<DashNestBumper>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> { with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
/// {@macro dash_nest_bumper_controller} /// {@macro dash_nest_bumper_controller}
DashNestBumperController( DashNestBumperController(
DashNestBumper dashNestBumper, { DashNestBumper dashNestBumper, {
required String id, required this.id,
}) : _id = id, }) : super(dashNestBumper);
super(dashNestBumper);
/// Unique identifier for the controlled [DashNestBumper]. /// Unique identifier for the controlled [DashNestBumper].
/// ///
/// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests].
final String _id; final String id;
@override @override
bool listenWhen(GameState? previousState, GameState newState) { bool listenWhen(GameState? previousState, GameState newState) {
final wasActive = previousState?.activatedDashNests.contains(_id) ?? false; final wasActive = previousState?.activatedDashNests.contains(id) ?? false;
final isActive = newState.activatedDashNests.contains(_id); final isActive = newState.activatedDashNests.contains(id);
return wasActive != isActive; return wasActive != isActive;
} }
@ -140,7 +140,7 @@ class DashNestBumperController extends ComponentController<DashNestBumper>
void onNewState(GameState state) { void onNewState(GameState state) {
super.onNewState(state); super.onNewState(state);
if (state.activatedDashNests.contains(_id)) { if (state.activatedDashNests.contains(id)) {
component.activate(); component.activate();
} else { } else {
component.deactivate(); component.deactivate();
@ -149,15 +149,14 @@ class DashNestBumperController extends ComponentController<DashNestBumper>
/// Registers when a [DashNestBumper] is hit by a [Ball]. /// Registers when a [DashNestBumper] is hit by a [Ball].
/// ///
/// Triggered by [ControlledDashNestBumperBallContactCallback]. /// Triggered by [_ControlledDashNestBumperBallContactCallback].
void hit() { void hit() {
gameRef.read<GameBloc>().add(DashNestActivated(_id)); gameRef.read<GameBloc>().add(DashNestActivated(id));
} }
} }
/// Listens when a [Ball] bounces bounces against a [DashNestBumper] /// Listens when a [Ball] bounces bounces against a [DashNestBumper]
@visibleForTesting class _ControlledDashNestBumperBallContactCallback
class ControlledDashNestBumperBallContactCallback
extends ContactCallback<Controls<DashNestBumperController>, Ball> { extends ContactCallback<Controls<DashNestBumperController>, Ball> {
@override @override
void begin( void begin(

@ -0,0 +1,34 @@
// ignore_for_file: cascade_invocations
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
group('BigDashNestBumper', () {
flameTester.test('loads correctly', (game) async {
final bigNest = BigDashNestBumper();
await game.ensureAdd(bigNest);
expect(game.contains(bigNest), isTrue);
});
});
group('SmallDashNestBumper', () {
flameTester.test('"a" loads correctly', (game) async {
final smallNest = SmallDashNestBumper.a();
await game.ensureAdd(smallNest);
expect(game.contains(smallNest), isTrue);
});
flameTester.test('"b" loads correctly', (game) async {
final smallNest = SmallDashNestBumper.b();
await game.ensureAdd(smallNest);
expect(game.contains(smallNest), isTrue);
});
});
}

@ -1,7 +1,12 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:math';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -9,6 +14,18 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
@ -30,33 +47,46 @@ void main() {
'a FlutterSignPost', 'a FlutterSignPost',
(game) async { (game) async {
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect( expect(
game.descendants().whereType<FlutterSignPost>().length, flutterForest.descendants().whereType<FlutterSignPost>().length,
equals(1), equals(1),
); );
}, },
); );
});
flameTester.test( flameTester.test(
'onNewState adds a new ball', 'a BigDashNestBumper',
(game) async { (game) async {
final flutterForest = FlutterForest();
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest); await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length; expect(
flutterForest.onNewState(MockGameState()); flutterForest.descendants().whereType<BigDashNestBumper>().length,
equals(1),
);
},
);
flameTester.test(
'two SmallDashNestBumper',
(game) async {
await game.ready(); await game.ready();
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
expect( expect(
game.descendants().whereType<Ball>().length, flutterForest.descendants().whereType<SmallDashNestBumper>().length,
greaterThan(previousBalls), equals(2),
); );
}, },
); );
});
group('controller', () {
group('listenWhen', () { group('listenWhen', () {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
final tester = flameBlocTester( final tester = flameBlocTester(
@ -64,14 +94,6 @@ void main() {
gameBloc: () => gameBloc, gameBloc: () => gameBloc,
); );
setUp(() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
tester.testGameWidget( tester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest is added',
verify: (game, tester) async { verify: (game, tester) async {
@ -85,7 +107,8 @@ void main() {
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
expect( expect(
flutterForest.listenWhen(const GameState.initial(), state), flutterForest.controller
.listenWhen(const GameState.initial(), state),
isTrue, isTrue,
); );
}, },
@ -93,15 +116,31 @@ void main() {
}); });
}); });
group('DashNestBumperBallContactCallback', () { flameTester.test(
final gameBloc = MockGameBloc(); 'onNewState adds a new ball',
final tester = flameBlocTester( (game) async {
// TODO(alestiago): Use TestGame.new once a controller is implemented. final flutterForest = FlutterForest();
game: PinballGameTest.create, await game.ready();
gameBloc: () => gameBloc, await game.ensureAdd(flutterForest);
final previousBalls = game.descendants().whereType<Ball>().length;
flutterForest.controller.onNewState(MockGameState());
await game.ready();
expect(
game.descendants().whereType<Ball>().length,
greaterThan(previousBalls),
);
},
); );
group('bumpers', () {
late Ball ball;
late GameBloc gameBloc;
setUp(() { setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -109,43 +148,170 @@ void main() {
); );
}); });
final dashNestBumper = MockDashNestBumper(); final tester = flameBlocTester<PinballGame>(
game: PinballGameTest.create,
gameBloc: () => gameBloc,
);
tester.testGameWidget( tester.testGameWidget(
'adds a DashNestActivated event with DashNestBumper.id', 'add DashNestActivated event',
setUp: (game, tester) async { setUp: (game, tester) async {
const id = '0'; final flutterForest = FlutterForest();
when(() => dashNestBumper.id).thenReturn(id); await game.ensureAdd(flutterForest);
when(() => dashNestBumper.gameRef).thenReturn(game); await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final controller = bumper.firstChild<DashNestBumperController>()!;
// TODO(alestiago): Investiagate why is is being called twice
// instead of once.
verify(
() => gameBloc.add(DashNestActivated(controller.id)),
).called(2);
}
}, },
verify: (game, tester) async { );
final contactCallback = DashNestBumperBallContactCallback();
contactCallback.begin(dashNestBumper, MockBall(), MockContact()); tester.testGameWidget(
'add Scored event',
setUp: (game, tester) async {
final flutterForest = FlutterForest();
await game.ensureAdd(flutterForest);
await game.ensureAdd(ball);
final bumpers =
flutterForest.descendants().whereType<DashNestBumper>();
for (final bumper in bumpers) {
beginContact(game, bumper, ball);
final points = (bumper as ScorePoints).points;
verify( verify(
() => gameBloc.add(DashNestActivated(dashNestBumper.id)), () => gameBloc.add(Scored(points: points)),
).called(1); ).called(1);
}
}, },
); );
}); });
});
group('DashNestBumperController', () {
late DashNestBumper dashNestBumper;
setUp(() {
dashNestBumper = MockDashNestBumper();
});
group(
'listensWhen',
() {
late GameState previousState;
late GameState newState;
setUp(
() {
previousState = MockGameState();
newState = MockGameState();
},
);
test('listens when the id is added to activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
group('BigDashNestBumper', () { when(() => previousState.activatedDashNests).thenReturn({});
test('has points', () { when(() => newState.activatedDashNests).thenReturn({id});
final dashNestBumper = BigDashNestBumper(id: '');
expect(dashNestBumper.points, greaterThan(0)); expect(controller.listenWhen(previousState, newState), isTrue);
}); });
test('listens when the id is removed to activatedDashNests', () {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isTrue);
}); });
group('SmallDashNestBumper', () { test("doesn't listen when the id is never in activatedDashNests", () {
group('has points', () { final controller = DashNestBumperController(
test('when a', () { dashNestBumper,
final dashNestBumper = SmallDashNestBumper.a(id: ''); id: '',
expect(dashNestBumper.points, greaterThan(0)); );
when(() => previousState.activatedDashNests).thenReturn({});
when(() => newState.activatedDashNests).thenReturn({});
expect(controller.listenWhen(previousState, newState), isFalse);
}); });
test('when b', () { test("doesn't listen when the id still in activatedDashNests", () {
final dashNestBumper = SmallDashNestBumper.b(id: ''); const id = '';
expect(dashNestBumper.points, greaterThan(0)); final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => previousState.activatedDashNests).thenReturn({id});
when(() => newState.activatedDashNests).thenReturn({id});
expect(controller.listenWhen(previousState, newState), isFalse);
}); });
},
);
group(
'onNewState',
() {
late GameState state;
setUp(() {
state = MockGameState();
}); });
test(
'activates the bumper when id in activatedDashNests',
() {
const id = '';
final controller = DashNestBumperController(
dashNestBumper,
id: id,
);
when(() => state.activatedDashNests).thenReturn({id});
controller.onNewState(state);
verify(() => dashNestBumper.activate()).called(1);
},
);
test(
'deactivates the bumper when id not in activatedDashNests',
() {
final controller = DashNestBumperController(
dashNestBumper,
id: '',
);
when(() => state.activatedDashNests).thenReturn({});
controller.onNewState(state);
verify(() => dashNestBumper.deactivate()).called(1);
},
);
},
);
}); });
} }
class MockDashNestBumper extends Mock implements DashNestBumper {}

Loading…
Cancel
Save