我有一个 StatefulWidget
,它根据加载状态(加载 -> 加载/错误)呈现不同的 Widget
:
// widget
class ListNotesScreen extends StatefulWidget {
static const route = '/listNotes';
static navigateTo(BuildContext context, [bool cleanStack = true]) =>
Navigator.pushNamedAndRemoveUntil(context, route, (_) => !cleanStack);
final String title;
final ListNotesUseCase _useCase;
final VoidCallback _addNoteCallback;
ListNotesScreen(this._useCase, this._addNoteCallback, {Key key, this.title}) : super(key: key);
@override
_ListNotesScreenState createState() => _ListNotesScreenState();
}
// state
class _ListNotesScreenState extends State<ListNotesScreen> {
ListNotesLoadState _state;
Future<ListNotesResponse> _fetchNotes() async {
return widget._useCase.listNotes();
}
@override
initState() {
super.initState();
_loadNotes();
}
_loadNotes() {
setState(() {
_state = ListNotesLoadingState();
});
_fetchNotes().then((response) {
setState(() {
_state = ListNotesLoadedState(response.notes);
});
}).catchError((error) {
setState(() {
_state = ListNotesLoadErrorState(error);
});
});
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('Notes list'),
actions: <Widget>[
IconButton(icon: Icon(Icons.add), onPressed: widget._addNoteCallback),
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadNotes())
],
),
body: _state.getWidget());
}
// loading states
// State:
@sealed
abstract class ListNotesLoadState {
Widget getWidget();
}
// Loading
class ListNotesLoadingState extends ListNotesLoadState {
@override
Widget getWidget() => Center(child: CircularProgressIndicator(value: null));
}
// Loaded
class ListNotesLoadedState extends ListNotesLoadState {
final List<Note> _notes;
ListNotesLoadedState(this._notes);
@override
Widget getWidget() => ListView.builder(
itemBuilder: (_, int index) => NoteItemWidget(this._notes[index]),
itemCount: this._notes.length,
padding: EdgeInsets.all(18.0));
}
这是小部件的测试:
void main() {
testWidgets('Notes list is shown', (WidgetTester tester) async {
final title1 = 'Title1';
final title2 = 'Title2';
final body1 = 'Body1';
final body2 = 'Body2';
var notes = [
Note('1', title1, body1),
Note('2', title2, body2),
];
final listUseCase = TestListNotesInteractor(notes);
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
expect(find.text('someInvalidString'), findsNothing);
expect(find.text(title1), findsOneWidget);
expect(find.text(title2), findsOneWidget);
expect(find.text(body1), findsOneWidget);
expect(find.text(body2), findsOneWidget);
// TODO: fix the test (tested manually and it works)
});
}
所以小部件测试器应该等到它在 initState()
中设置为加载的状态,然后 _loadNotes
将其移动到 ListNotesLoadedState
并且ListNotesLoadedState.getWidget()
返回带有预期字符串的 ListView
(NoteItemWidget
root 和一些带有预期字符串的 Text
)。
但是测试失败了。原因是什么(我能够在应用程序中使用测试交互器并直观地看到预期的文本)?我如何在测试失败时分析实际的 Widgets 树?
我倾向于认为 WidgetTester 没有等待 Future
完成(虽然它预计会被模拟并在幕后同步,请纠正我)。
可以找到 project on Github (确保调用 flutter packages pub run build_runner build
生成 json 反序列化代码)。
最佳答案
我找到了原因:MaterialApp
(或者可能是任何应用程序)应该是小部件树的根!
final widget = MaterialApp(home: ListNotesScreen(interactor, null)); // succeeds
代替:
final widget = ListNotesScreen(interactor, null); // fails
我还删除了未使用的 title
属性,因此测试代码与我最初使用的代码有点不同:
final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
the docs 中没有提到(实际上是这个原因吗?)虽然测试代码有它。如果我遗漏了什么,请告诉我。
关于unit-testing - Flutter 的 WidgetTester.pumpAndSettle() 不等待渲染完成?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59339054/