-
Notifications
You must be signed in to change notification settings - Fork 6
Description
We want to use context plus for dependency injection in tests.
We have one MyApp widget and want to inject a either use a ProdApi or a FakeApi class.
// Sample without dependency injection
import 'package:context_plus/context_plus.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
final Ref<Api> apiRef = Ref<Api>();
abstract class Api {
Future<void> sendHeartbeat();
}
class ProdApi implements Api {
@override
Future<void> sendHeartbeat() async {
// Simulate sending a heartbeat to the production server
print('Heartbeat sent to production server');
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => ProdApi());
return MaterialApp(
home: HomeScreen(),
);
}),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
apiRef.of(context).sendHeartbeat();
},
child: const Text('Send to Server'),
),
],
),
),
);
}
}
Currently, ContextPlus.root lives within MyApp. This is fine for production, but we can't inject from the outside in our test environment
class FakeApi implements Api {
@override
Future<void> sendHeartbeat() async {
throw Exception('cannot reach server');
}
}
void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = Builder(builder: (context) {
// Fails with "No ContextRef.root() found. Did you forget to add a ContextRef.root() widget?"
apiRef.bind(context, () => FakeApi());
return const MyApp();
});
await tester.pumpWidget(app);
});
}There are two ways to solve this
Move ContextRef.root
Remove ContextPlus.root and dependencies from MyApp and move it up to the main function
void main() {
runApp(ContextPlus.root(
child: Builder(
builder: (context) {
apiRef.bind(context, () => ProdApi());
return MyApp();
},
),
));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}Add ContextPlus.root in the test
void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => FakeApi());
return const MyApp();
}),
);
await tester.pumpWidget(app);
});
}
Wrap with another ContextRef.root
Keep ContextPlus.root in MyApp but use bindWhenUnbound instead of bind.
In tests, add another ContextPlus.root to inject fakes.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bindWhenUnbound(context, () => ProdApi());
return MaterialApp(
home: HomeScreen(),
);
}),
);
}
}void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => FakeApi());
return const MyApp();
}),
);
await tester.pumpWidget(app);
});
}extension BindUnboundRef<T> on Ref<T> {
/// Binds the Ref if it is not already bound to [context], otherwise returns the existing value from [context].
T bindWhenUnbound(
BuildContext context,
T Function() create, {
void Function(T value)? dispose,
Object? key,
}) {
try {
T found = of(context);
return found;
} catch (e) {
// If the Ref is not bound, bind it now
return bind(context, create, dispose: dispose, key: key);
}
}
}I do prefer wrapping again with ContextPlus.root and it seems to work fine. But I'm not 100% sure about the internals, if this case is accounted for. I couldn't find tests for this case. That's why I'm coming here to ask if anything could go wrong with nested ContextPlus.root.
In case this is completely fine, I suggest:
- Add test cases for nested
ContextPlus.root - Add an actual Exception like
ProviderNotFoundExceptionwhich I can catch inbindWhenUnbound.