I'm learning flutter's widget testing
and I'm reading flutter's official documentation of widget testing.
I wonder how find() method finds the widget.
find.byKey() may find by look around widget tree that which widget has specific key,
but like find.byWidget() how does it finds specific widget?
The find.byWidget() method finds a widget by checking if it is equal to the widget passed as an argument to the method. This is typically done by checking if the runtimeType and key of the two widgets match.
For example, let's say you have a Text widget with a specific key and you want to find it using the find.byWidget() method. You would first create the Text widget and assign it a key, like so:
final myText = Text(
'Hello World',
key: Key('my_text'),
);
Then, you can use the find.byWidget() method to find this widget in the widget tree, like this:
final foundWidget = find.byWidget(myText);
This will return the Text widget if it is found in the widget tree, or null if it is not found. You can then use this widget reference to perform assertions or interact with the widget in your tests.
Keep in mind that the find.byWidget() method only checks for strict equality between the two widgets. This means that if you have multiple widgets with the same runtimeType and key, the find.byWidget() method will only return the first widget it encounters that matches the criteria. To find all widgets that match the criteria, you can use the find.descendant() method instead.
going inside Flutter's source code, we will find that the find.byWidget() is implemented like this:
Finder byWidget(Widget widget, { bool skipOffstage = true }) => _WidgetFinder(widget, skipOffstage: skipOffstage);
going more inside _WidgetFinder:
class _WidgetFinder extends MatchFinder {
_WidgetFinder(this.widget, { super.skipOffstage });
final Widget widget;
#override
String get description => 'the given widget ($widget)';
#override
bool matches(Element candidate) {
return candidate.widget == widget;
}
}
so for flutter to find a specific widget, it compares it with the == operator with the one you provided.
Related
I am trying to replace the use of Stateful widget with Getx. Most cases, I do not need it, however, when I am trying to create a list of cards widget, I find it hard to not to use the Stateful widget.
Obx(() {
getxA.updateA(name);
return Text(getxA.a());
})
RxString a = 'aaa'.obs;
updateA(String name) {
a(name);
}
When I have this Obx() - Text Widget inside the List Widget, it causes the error because each Text widget triggers updateA(String name) function.
It looks like every card is sharing one GetxController (getxA). Is there any way that to have each Text widget have its own GetxController? Or is it not possible in this case and I have to use the Stateful widget?
There are two ways in my mind to return widget which is repeating again and again. lets see with the example for better understanding. if there is a container which is repeating multiple times with only text changing to if we apply OOP concepts we can refactor the code by extracting container widget and call it wherever we need but there are two ways (in my knowledge) to do this task both works fine but what would be the best practice?
Widget returnContainer(String text){
return Container(....);
}
or creating stateless widget and return container
class ReturnContainer extends StatelessWidget {
final String text;
ReturnContainer(this.text);
#override
Widget build(BuildContext context) {
return Container(.....);
}
}
They're both valid solutions, but apply to different situations.
You would choose the return function if your widget needs to be called only in the dart file where you are implementing it.
You would otherwise choose a stateless widget if your code needs to be used many times in manu different files.
I am trying to create tests for my Flutter application. Simple example:
class MyWidget extends StatelessWidget {
#override
build(BuildContext context) {
return MySecondWidget();
}
}
I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget.
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider...). What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.
How can I do something like that ?
There is nothing done out of the box to mock a widget. I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink().
But first, let me explain why I think this is not a good idea.
In Flutter you are building a widget tree. A specific widget has a parent and usually has one or several children.
Flutter chose a single pass layout algorithm for performance reasons (see this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly. If you remove its children, it might behave completely differently.
It is better to mock the services if possible. For example, if your child makes an HTTP request, you can mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
If the child needs a specific provider you can provide a dummy one:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
If you still want to change a widget with another one during your tests, here are some ideas:
1. Use Platform.environment.containsKey('FLUTTER_TEST')
You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).
and your build method could be:
#override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2. Use the annotation #visibleForTesting
You can annotate a parameter (ex: mockChild) that is only visible/usable in a test file:
class MyWidget extends StatelessWidget {
const MyWidget({
#visibleForTesting this.mockChild,
});
final Widget? child;
#override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
And in your test:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);
You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty. Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.
One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
#override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.
I already known that build context can be used in StatefulWidget any where but only in build function when using Stateless Widget. There is so many content in widget need to reference the build context like Theme, showDialog,Navigator,Provider...
For Example, I have some code below in StatelessWidget:
#override
Widget build(BuildContext context){
...
_getFirstWidget();
...
}
...
Widget _getFirstWidget(){
return _getSecondWidget();
}
Widget _getSecondWidget(){
return _getThirdWidget();
}
Widget _getThirdWidget(){
// use build context here
}
...
If I want to use the build context at the end Widget, I think of 3 ways:
Pass the build context layer by layer
Convert to StatefulWidget
Convert the last widget to a Stateless Widget itself (and use the build context in build)
Why flutter make this restriction in StatelessWidget?
I'm not really sure but I think you want the use the BuildContext from the build method in the function '_getThirdWidget()'. You could just pass it as a parameter like below:
Widget _getThirdWidget(BuildContext context) {
// Use the context here
}
// Call the function like this in the parent widget
#override
Widget build(BuildContext context) {
return _getThirdWidget(context);
}
Let me know if this answers your question!
If you use the method of adding an argument to use context,
Almost every function needs a context argument
this is stupid behavior
StatelessWidget is inconvenient
I try to use StatelessWidget, but end up using Statefulwidget
The docs here about how to update a ListView say:
In Flutter, if you were to update the list of widgets inside a
setState(), you would quickly see that your data did not change
visually. This is because when setState() is called, the Flutter
rendering engine looks at the widget tree to see if anything has
changed. When it gets to your ListView, it performs a == check, and
determines that the two ListViews are the same. Nothing has changed,
so no update is required.
For a simple way to update your ListView, create a new List inside of
setState(), and copy the data from the old list to the new list.
I don't get how the Render Engine determines if there are any changes in the Widget Tree in this case.
AFAICS, we care calling setState, which marks the State object as dirty and asks it to rebuild. Once it rebuilds there will be a new ListView, won't it? So how come the == check says it's the same object?
Also, the new List will be internal to the State object, does the Flutter engine compare all the objects inside the State object? I thought it only compared the Widget tree.
So, basically I don't understand how the Render Engine decides what it's going to update and what's going to ignore, since I can't see how creating a new List sends any information to the Render Engine, as the docs says the Render Engine just looks for a new ListView... And AFAIK a new List won't create a new ListView.
Flutter isn't made only of Widgets.
When you call setState, you mark the Widget as dirty. But this Widget isn't actually what you render on the screen.
Widgets exist to create/mutate RenderObjects; it's these RenderObjects that draw your content on the screen.
The link between RenderObjects and Widgets is done using a new kind of Widget: RenderObjectWidget (such as LeafRenderObjectWidget)
Most widgets provided by Flutter are to some extent a RenderObjectWidget, including ListView.
A typical RenderObjectWidget example would be this:
class MyWidget extends LeafRenderObjectWidget {
final String title;
MyWidget(this.title);
#override
MyRenderObject createRenderObject(BuildContext context) {
return new MyRenderObject()
..title = title;
}
#override
void updateRenderObject(BuildContext context, MyRenderObject renderObject) {
renderObject
..title = title;
}
}
This example uses a widget to create/update a RenderObject. It's not enough to notify the framework that there's something to repaint though.
To make a RenderObject repaint, one must call markNeedsPaint or markNeedsLayout on the desired renderObject.
This is usually done by the RenderObject itself using custom field setter this way:
class MyRenderObject extends RenderBox {
String _title;
String get title => _title;
set title(String value) {
if (value != _title) {
markNeedsLayout();
_title = value;
}
}
}
Notice the if (value != previous).
This check ensures that when a widget rebuilds without changing anything, Flutter doesn't relayout/repaint anything.
It's due to this exact condition that mutating List or Map doesn't make ListView rerender. It basically has the following:
List<Widget> _children;
List<Widget> get children => _children;
set children(List<Widget> value) {
if (value != _children) {
markNeedsLayout();
_children = value;
}
}
But it implies that if you mutate the list instead of creating a new one, the RenderObject will not be marked as needing a relayout/repaint. Therefore there won't be any visual update.