Reputation: 5429
I have a StatefulWidget (call it MyWidget
) whose State (MyWidgetState
) has a field myData
which is initialized during initState()
as follows:
void initState() {
super.initState();
myData = new myData(config.someField.getId());
}
When the user presses a button, myData is added to or removed from a global list.
I'm trying to write a unit test to test this behavior but I don't know how to get access to a MyWidgetState. I tried including this in the setup():
widget = MyWidget();
widgetState = widget.createState();
widgetState.init();
but it crashes every time when it tries to initState(), complaining that "someField was called on null". That's fine. I was probably cheating by trying to do it that way and I ought to do something with a WidgetBuilder or launch an application using MyWidget and then find MyWidget in the tree once it's properly instantiated.
If I do all of that, once I do how can I access that MyWidget's MyWidgetState to get a copy of myData and compare it to the global list?
Upvotes: 24
Views: 14591
Reputation: 1872
Here is one full, working example of testing state values in a widget test.
This might not be the ideal approach since both the widget state and state variable are public, but it works. Specifically, ensure that both the widget state and state variables are public for the test to access them, so no leading underscore (_
).
// main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
});
@override
State<MyHomePage> createState() => MyHomePageState();
}
// Note: this must not be a private member,
// so no leading underscore (_)
class MyHomePageState extends State<MyHomePage> {
// Note: this must not be a private member,
// so no leading underscore (_)
int counter = 0;
void _incrementCounter() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:state_test/main.dart';
void main() {
testWidgets('HomePage counter increments when + button pressed', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Get widget state for validation
final MyHomePageState myHomePageState =
tester.state(find.byType(MyHomePage));
// Verify that our counter starts at 0
expect(myHomePageState.counter, 0);
// Tap the '+' icon to increment the counter
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented
expect(myHomePageState.counter, 1);
});
}
Upvotes: 1
Reputation: 2203
If you want to write a unit test on one of the methods of your widget's state, here's how it can be done:
// Assuming your stateful widget is like this:
class MyWidget extends StatefulWidget {
const MyWidget({this.someParam, Key? key}) : super(key: key);
final String someParam;
@override
MyWidgetState createState() => MyWidgetState();
}
@visibleForTesting
class MyWidgetState extends State<MyWidget> {
int methodToBeTested() {
// dummy implementation that uses widget.XXX
if (widget.someParam.isEmpty) return 0;
return 1;
}
@override
Widget build(BuildContext context) {
// return ...
}
}
// In your test file
void main() {
test('if widget.someParam is empty, do something', () {
const widget = MyWidget(someParam: '');
final element = widget.createElement(); // this will set state.widget
final state = element.state as MyWidgetState;
expect(state.methodToBeTested(), 0);
});
}
Upvotes: 10
Reputation: 21965
If you use any code snippets to create a stateful widget in Flutter, you might have noticed that the state class created by Flutter is a private class which is maked by an underscore in the beginning. This is good because such classes are not meant to be used outside the library - well except for testing.
In widget/integration testing, you might wish to access the variables of the state class. In such case, marking the state class as private would mean that you cannot directly access those variables. This may not be desired. To get the best of both worlds, we can use @visibleForTesting
decorator.
An example is given below.
import 'dart:math';
import 'package:flutter/material.dart';
class CounterWidget extends StatefulWidget {
const CounterWidget({Key key}) : super(key: key);
@override
CounterWidgetState createState() => CounterWidgetState();
}
@visibleForTesting
class CounterWidgetState extends State<CounterWidget> {
int counter;
@override
void initState() {
super.initState();
var rndGen = Random(79);
counter = rndGen.nextInt(96);
}
@override
Widget build(BuildContext context) {
return Container(
child: ElevatedButton(
key: Key('incrementButton'),
onPressed: () {
setState(() {
counter++;
});
},
child: Text(
'Increment($counter)',
)));
}
}
The documentation for @visibleForTesting
says
Used to annotate a declaration that was made public, so that it is more visible than otherwise necessary, to make code testable.
Tools, such as the analyzer, can provide feedback if
the annotation is associated with a declaration not in the lib folder of a package, or a private declaration, or a declaration in an unnamed static extension, or the declaration is referenced outside of its defining library or a library which is in the test folder of the defining package.
The key takeaway here is that the Dart analyzer will warn you if you use your public state class outside your library or tests, which is seldom a requirement in Flutter design.
The idea of using visibleForTesting
came from here
Upvotes: 3
Reputation: 359
You can create a state and then access it's content. Following Ian Hickson answer. hereunder is an example for the implementation:
final MyWidgetState myWidgetState = tester.state(find.byType(MyWidget));
Then you can access the state content:
myWidgetState.myData;
You can find more examples in the Flutter's repo.
Upvotes: 32
Reputation: 8609
Create the widget using tester.pumpWidgets
, then use tester.state(find.foo)
to find the State
(where find.foo
is a finder that finds the widget). See the WidgetTester
documentation for more options.
Upvotes: 12