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
ProviderNotFoundException which I can catch in bindWhenUnbound.
We want to use context plus for dependency injection in tests.
We have one
MyAppwidget and want to inject a either use aProdApior aFakeApiclass.Currently,
ContextPlus.rootlives withinMyApp. This is fine for production, but we can't inject from the outside in our test environmentThere are two ways to solve this
Move ContextRef.root
Remove
ContextPlus.rootand dependencies from MyApp and move it up to themainfunctionAdd
ContextPlus.rootin the testWrap with another ContextRef.root
Keep
ContextPlus.rootinMyAppbut usebindWhenUnboundinstead ofbind.In tests, add another
ContextPlus.rootto inject fakes.I do prefer wrapping again with
ContextPlus.rootand 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 nestedContextPlus.root.In case this is completely fine, I suggest:
ContextPlus.rootProviderNotFoundExceptionwhich I can catch inbindWhenUnbound.