How to create Widget Tests in Flutter
Widget testing is an important part of building reliable Flutter applications. It helps ensure that individual widgets work as intended. In this post, we will explore widget testing by testing a custom widget called AuthenticationTextFormField
, which is used for login and registration. You will learn to set up, write and organize widget tests. At the end of the post you will be able to create widget tests for your own widgets with ease.
AuthenticationTextFormField
As said in the introduction we will be writing widget test for the AuthenticationTextFormField
widget. This widget has properties, validation and styling that needs to be tested, let us have a look:
import 'package:flutter/material.dart';
class AuthenticationTextFormField extends StatelessWidget {
const AuthenticationTextFormField({
Key? key,
this.confirmationController,
required this.icon,
required this.label,
required this.textEditingController,
}) : super(key: key);
final TextEditingController? confirmationController;
final IconData icon;
final String label;
final TextEditingController textEditingController;
String? validate({required String? value}) {
if (value!.isEmpty) {
return 'This field cannot be empty.';
}
if ((key.toString().contains('email')) &&
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value) == false) {
return 'This is not a valid email address.';
}
if ((key.toString().contains('password')) && value.length < 6) {
return 'The password must be at least 6 characters.';
}
if ((key.toString().contains('password_confirmation')) &&
value != confirmationController?.text) {
return 'The password does not match.';
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: textEditingController,
obscureText: label.toLowerCase().contains('password'),
decoration: InputDecoration(
errorStyle: const TextStyle(fontSize: 14),
floatingLabelStyle: const TextStyle(fontSize: 20),
icon: Icon(
icon,
color: Theme.of(context).primaryColor,
),
labelText: label,
),
validator: (value) => validate(value: value),
);
}
}
In this code snippet, we have AuthenticationTextFormField
widget, that returns a FormTextField
widget. The AuthenticationTextFormField
is designed to be used in an authentication page, therefore it has a validation function. In the validation function, we validate the different possibilities, because the field can be an email, password or password confirmation field.
In the build
function we return a TextFormField
widget, where we assign the controller
property to the provided textEditingController
. The obscureText
property is set to true
when the provided label contains password
. We also provided additional styling by setting the decoration
property which also takes the provided icon
and label
. At last we set the validator property which makes sure that our validation function is applied on the TextFormField
.
In the build function, we use a TextFormField
widget. We connect the widget to the given textEditingController
for handling the text input. When the label contains "password"
, we make the input text hidden for security. Additionally, we add styling by setting the decoration
property which takes the provided icon
and label
. Finally, we apply our validate
function using the validator property to ensure correct input in the TextFormField
widget.
In the GIF below you can see what the AuthenticationFormTextField
looks like:

If you are interested in learning how I built this authentication screen, please refer to this post:

Widget tests
Now that we covered the widget we are going to test, let us begin writing tests. A best practice is to name the test files after the widgets being tested, adding the "test" suffix. For example, in this case, the test file will be named authentication_text_form_field_test.dart
. Another good practice is to create a widget
or widgets
directory within your test
directory to organize your widget tests.
1. Creation tests
The first tests we will create ensures that the AuthenticationTextFormField
widget has the right properties and contains the necessary sub-widgets. Let us take a look at the code:
import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TextEditingController textEditingController = TextEditingController();
TextEditingController confirmationController = TextEditingController();
Widget widgetUnderTest = MaterialApp(
home: Material(
child: AuthenticationTextFormField(
icon: Icons.code,
label: 'test',
textEditingController: textEditingController,
confirmationController: confirmationController,
key: const ValueKey('email'),
),
),
);
group('creation', () {
testWidgets('it render the right sub widgets', (tester) async {
await tester.pumpWidget(widgetUnderTest);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
});
testWidgets('it instantiates with the right properties', (tester) async {
await tester.pumpWidget(widgetUnderTest);
Finder authField = find.byType(AuthenticationTextFormField);
AuthenticationTextFormField authFieldWidget = tester.widget(authField);
expect(authFieldWidget.icon, Icons.code);
expect(authFieldWidget.label, 'test');
expect(authFieldWidget.textEditingController, textEditingController);
expect(authFieldWidget.confirmationController, confirmationController);
expect(authFieldWidget.key, const ValueKey('email'));
});
});
}
Let us go over the code snippet in detail so that we know exactly what is going on:
1.1 Import statements
import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_test/flutter_test.dart';
Description: In this section, the necessary packages are imported. The code imports flutter/material.dart
for Flutter widgets, flutter_authentication_screen/widgets/authentication_text_form_field.dart
for the custom widget AuthenticationTextFormField
, and flutter_test/flutter_test.dart
for performing widget tests.
1.2 Test setup
void main() {
TextEditingController textEditingController = TextEditingController();
TextEditingController confirmationController = TextEditingController();
Widget widgetUnderTest = MaterialApp(
home: Material(
child: AuthenticationTextFormField(
icon: Icons.code,
label: 'test',
textEditingController: textEditingController,
confirmationController: confirmationController,
key: const ValueKey('email'),
),
),
);
}
Description: The main function serves as the entry point for the test. Here, two TextEditingController
instances, textEditingController
and confirmationController
, are created. These controllers will be used as arguments for the AuthenticationTextFormField
widget when it is tested. A Widget
called widgetUnderTest
is defined, which contains an instance of the AuthenticationTextFormField
.
1.3 Grouping and Test Cases
group('creation', () {
testWidgets('it render the right sub widgets', (tester) async {
await tester.pumpWidget(widgetUnderTest);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
});
testWidgets('it instantiates with the right properties', (tester) async {
await tester.pumpWidget(widgetUnderTest);
Finder authField = find.byType(AuthenticationTextFormField);
AuthenticationTextFormField authFieldWidget = tester.widget(authField);
expect(authFieldWidget.icon, Icons.code);
expect(authFieldWidget.label, 'test');
expect(authFieldWidget.textEditingController, textEditingController);
expect(authFieldWidget.confirmationController, confirmationController);
expect(authFieldWidget.key, const ValueKey('email'));
});
});
Description: The test cases are organized using the group
function. Here, the tests are grouped under the label "creation". Within this group, we define two test cases:
- The first test case verifies that the
widgetUnderTest
renders the correct sub-widgets, specificallyTextFormField
andIcon
. - The second test case checks that the
AuthenticationTextFormField
is instantiated with the correct properties and values. It compares the properties of theAuthenticationTextFormField
widget, such asicon
,label
,textEditingController
,confirmationController
, andkey
, with the expected values.

Overall, this code snippet demonstrates how to set up and write widget tests. The written tests ensures that the widget renders the correct sub-widgets and is instantiated with the expected properties.
2. Refactor
We can improve the previous code snippet by creating a function that will create our widgetUnderTest
so that we can provide parameters and configure the AuthenticationTextFormField
widget with different parameters:
import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TextEditingController textEditingController = TextEditingController();
TextEditingController confirmationController = TextEditingController();
Future<void> createWidgetUnderTest(WidgetTester tester, {
String label = 'test',
String key = 'email',
}) async {
Widget widgetUnderTest = MaterialApp(
home: Material(
child: AuthenticationTextFormField(
icon: Icons.code,
label: label,
textEditingController: textEditingController,
confirmationController: confirmationController,
key: ValueKey(key),
),
),
);
await tester.pumpWidget(widgetUnderTest);
}
group('creation', () {
testWidgets('it render the right sub widgets', (tester) async {
await createWidgetUnderTest(tester);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
});
testWidgets('it instantiates with the right properties', (tester) async {
await createWidgetUnderTest(tester);
Finder authField = find.byType(AuthenticationTextFormField);
AuthenticationTextFormField authFieldWidget = tester.widget(authField);
expect(authFieldWidget.icon, Icons.code);
expect(authFieldWidget.label, 'test');
expect(authFieldWidget.textEditingController, textEditingController);
expect(authFieldWidget.confirmationController, confirmationController);
expect(authFieldWidget.key, const ValueKey('email'));
});
});
}
Let us go over the code snippet in detail so that we know exactly what was changed:
2.1 Function for Widget Creation
Future<void> createWidgetUnderTest(
WidgetTester tester, {
String label = 'test',
String key = 'email',
}) async {
Widget widgetUnderTest = MaterialApp(
home: Material(
child: AuthenticationTextFormField(
icon: Icons.code,
label: label,
textEditingController: textEditingController,
confirmationController: confirmationController,
key: ValueKey(key),
),
),
);
await tester.pumpWidget(widgetUnderTest);
}
Differences: The improved code snippet presents a new function named createWidgetUnderTest
. This function simplifies the process of creating the widget under test. It accepts two optional parameters: label
and key
, which allow customization of the AuthenticationTextFormField
widget's properties. The goal of this function is to make the test code more straightforward by handling the widget creation steps and making it easier to test the widget with different properties.
2.2 Test Case Modifications
testWidgets('it render the right sub widgets', (tester) async {
await createWidgetUnderTest(tester);
...
});
testWidgets('it instantiates with the right properties', (tester) async {
await createWidgetUnderTest(tester);
...
});
Differences: In the improved code snippet, the test cases have been modified to use the createWidgetUnderTest
function to create the widget for testing. Instead of directly calling pumpWidget
in each test case, they now call the createWidgetUnderTest
function with tester
as a parameter. This reduces code duplication and improves the maintainability of the tests.
Additionaly, it also allows us to create the following test much easier:
2.3 Customization of Widget Properties in Tests
group('creation', () {
...
testWidgets('it obscures text when label contains password',
(tester) async {
await createWidgetUnderTest(tester, label: 'password', key: 'password');
Finder editableText = find.byType(EditableText);
EditableText editableTextWidget = tester.widget(editableText);
expect(editableTextWidget.obscureText, isTrue);
});
});
Description: The new test case verifies that the AuthenticationTextFormField
obscures the text when the label contains the word 'password'.
- The test case uses the
createWidgetUnderTest
function with thelabel
andkey
parameters set to'password'
to create the widget. Then, it looks for anEditableText
widget in the widget tree and checks its properties. We do this because theTextFormField
widget usesEditableText
to hide the password. Finally, we useexpect
to make sure that theobscureText
property ofEditableText
istrue
, which ensures that the password text is hidden.

With the addition of the third test case, the widget test suite now includes a test to ensure that the text in AuthenticationTextFormField
is obscured when the label contains the word 'password'. This enhances the test coverage and validates a specific behavior of the widget related to password fields.
3. Validation tests
To finished it up let us create some tests for the validation function, see the code below:
group('validation', () {
testWidgets('it displays an error message when field is empty',
(tester) async {
await createWidgetUnderTest(tester);
String value = '';
Finder textFormField = find.byType(TextFormField);
await tester.enterText(textFormField, value);
TextFormField textFormFieldWidget = tester.widget(textFormField);
expect(
textFormFieldWidget.validator!(value),
'This field cannot be empty.',
);
});
testWidgets('it displays an error message when email field is invalid',
(tester) async {
await createWidgetUnderTest(tester);
String value = 'test.test';
Finder textFormField = find.byType(TextFormField);
await tester.enterText(textFormField, value);
TextFormField textFormFieldWidget = tester.widget(textFormField);
expect(
textFormFieldWidget.validator!(value),
'This is not a valid email address.',
);
});
});
Description: In this section, two additional test cases are added under the "validation" group. These test cases focus on validating the behavior of the AuthenticationTextFormField
when validating user input:
- The first test case checks if an error message is displayed when the field is empty. It simulates entering an empty value in the
AuthenticationTextFormField
usingtester.enterText
and then retrieves theTextFormField
widget from the widget tree. Thevalidator
function of theTextFormField
is called with the enteredvalue
, and an assertion is made to ensure that the expected error message'This field cannot be empty.'
is displayed. - The second test case verifies that an error message is shown when an invalid email address is entered. Similar to the first test case, it enters a value
'test.test'
into theAuthenticationTextFormField
, retrieves theTextFormField
widget, and calls itsvalidator
function with the entered value (value
). The test checks if the expected error message'This is not a valid email address'
. is displayed.

These test cases help ensure that the AuthenticationTextFormField
properly validates user input and displays the correct error messages when needed. They provide a way to test the validation logic of the widget.
Conclusion
In this post, we explored how to write widget tests in Flutter, using the AuthenticationTextFormField
as our example. We learned about test file naming and organization best practices. We went over the basic set up and created our first tests. By refactoring the code we made our tests easier to manage. Additionally, we validated the widget's input handling and error message display.
While the five tests we created may not cover all scenarios for the AuthenticationFormText
widget, they provide a foundation for creating your own widget tests to ensure the reliability and accuracy of your widgets.