11

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?

Reagankm
  • 4,093
  • 6
  • 26
  • 45

4 Answers4

20

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.

user2181452
  • 231
  • 3
  • 7
  • 3
    This should be selected as the correct answer. However, you're missing something. In order for accessing the state properties correctly, when you get the state you have to specify the state's type: `tester.state(find.byType(MyWidget));` Then, you would be able to access its properties without any errors popping up. – Javi Marzán Apr 16 '20 at 08:49
  • I was wrong in my previous comment but cannot edit it. You're answer is completely correct, you only have to specify the state type if you declare the variable as `final` (and let Flutter infer the type). If you write `final MyWidgetState` as you stated it's perfectly correct :) – Javi Marzán Apr 16 '20 at 12:53
8

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.

Ian Hickson
  • 7,430
  • 1
  • 22
  • 19
5

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);
  });
}
Gpack
  • 1,320
  • 1
  • 15
  • 39
1

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

sjsam
  • 20,774
  • 4
  • 49
  • 94