A month of Flutter: testing forms

With the new user registration form in place, it's time to make sure the form is tested and will work as expected.

There are basically five different states that need to be tested.

Default state

This is the view users will first arrive to and here I'm testing that all the components are present as expected.

testWidgets('Renders', (WidgetTester tester) async {
  await tester.pumpWidget(app);

  expect(find.text('Register'), findsOneWidget);
  expect(find.text('I agree to the Terms of Services and Privacy Policy'),
      findsOneWidget);
  expect(find.byType(TextFormField), findsNWidgets(2));
  expect(find.byType(OutlineButton), findsOneWidget);
  expect(find.byType(Checkbox), findsOneWidget);
});

Submitted

To succesfully submit the form, I require the user to provide a nickname and a full name. In the test those values will be provided with enterText. After filling out the two TextFormFields and submitting the form, I wait a tick for the success SnackBar to render.

testWidgets('Form can be submitted', (WidgetTester tester) async {
  await tester.pumpWidget(app);
  final Finder nickname = find.widgetWithText(TextFormField, 'Nickname');
  final Finder fullName = find.widgetWithText(TextFormField, 'Full name');
  final Finder submit = find.widgetWithText(OutlineButton, 'Register');

  expect(find.text('Form submitted'), findsNothing);

  await tester.enterText(nickname, 'Jess');
  await tester.enterText(fullName, 'Jess Sampson');

  await tester.tap(submit);
  await tester.pump();

  expect(find.text('Form submitted'), findsOneWidget);
});

The success SnackBar is a temporary placeholder so that I have an in-app confirmation the form was submitted. Once user registration logic is in place this messaging to the user will change.

void _submit() {
  if (_formKey.currentState.validate()) {
    const SnackBar snackBar = SnackBar(content: Text('Form submitted'));

    Scaffold.of(context).showSnackBar(snackBar);
  }
}

Required fields

Next there are two tests to make sure the nickname and full name fields are required. This checks that the required message was displayed and that the success message was not displayed.

testWidgets('Form requires nickname', (WidgetTester tester) async {
  await tester.pumpWidget(app);
  final Finder submit = find.widgetWithText(OutlineButton, 'Register');
  await tester.tap(submit);
  await tester.pump();

  expect(find.text('Nickname is required'), findsOneWidget);
  expect(find.text('Form submitted'), findsNothing);
});

There are some improvements that could be made this error display. If a user focuses an errored TextFormField and enters a letter, the error message should disappear. I've created an issue to implement this in the future.

Disabled submit

If a user disables the Terms of Service/Privacy Policy checkbox, they are not longer permitted to register. Here I am testing that the submit button is disabled if the checkbox is unchecked.

The WidgetTester#widget method is a way to get a reference to a finder's actual widget. This is how I'm testing to see if the submit button is disabled. Usually you should test UI that the user can see but in this case the state of the button is conveyed to the user through its styling.

testWidgets('Submit disabled if TOS unchecked', (WidgetTester tester) async {
  await tester.pumpWidget(app);
  final Finder submit = find.widgetWithText(OutlineButton, 'Register');
  final Finder tos = find.byType(Checkbox);

  expect(tester.widget<OutlineButton>(submit).enabled, isTrue);

  await tester.tap(tos);
  await tester.tap(submit);
  await tester.pump();

  expect(tester.widget<OutlineButton>(submit).enabled, isFalse);
  expect(find.text('Form submitted'), findsNothing);
});

Code changes

Posts in this series

  • A month of Flutter
  • A month of Flutter: create the app
  • A month of Flutter: configuring continuous integration
  • A month of Flutter: continuous linting
  • A month of Flutter: upgrading to 1.0
  • A month of Flutter: initial theme
  • A month of Flutter: no content widget
  • A month of Flutter: a list of posts
  • A month of Flutter: extract post item widget
  • A month of Flutter: post model and mock data
  • A month of Flutter: rendering a ListView with StreamBuilder
  • A month of Flutter: Stream transforms and failing tests
  • A month of Flutter: real faker data
  • A month of Flutter: rendering network images
  • A month of Flutter: FABulous authentication
  • A month of Flutter: configure Firebase Auth for Sign in with Google on Android
  • A month of Flutter: configure Firebase Auth for Sign in with Google on iOS
  • A month of Flutter: Sign in with Google
  • A month of Flutter: mocking Firebase Auth in tests
  • A month of Flutter: delicious welcome snackbar
  • A month of Flutter: navigate to user registration
  • A month of Flutter: user registration form
  • A month of Flutter: testing forms
  • A month of Flutter: setting up Firebase Firestore
  • A month of Flutter: awesome adaptive icons
  • A month of Flutter: set up Firestore rules tests
  • A month of Flutter: Firestore create user rules and tests
  • A month of Flutter: WIP save users to Firestore
  • A month of Flutter: user registration refactor with reactive scoped model
  • A month of Flutter: the real hero animation
  • A month of Flutter: a look back