Widget Testing in Flutter: Writing Independent, Readable, and Fast Tests

Written by azamatnurkhojayev | Published 2024/01/12
Tech Story Tags: flutter | tutorial | flutter-tutorial | flutter-widgets | widget-testing | unit-tests | flutter-development | mobile-app-testing

TLDRWidget tests are used to test if some widgets are present in the widget tree or to test interactions with widgets.via the TL;DR App

There are 3 types of tests in Flutter:

  1. Unit tests - ordinary unit tests where we write tests for our functions, methods, and classes.
  2. Widget tests - we write tests for our widgets and check whether they match visually, and you can also check an event, such as a click.
  3. Integration tests - as a rule, the entire application is tested and launched on real devices or in an emulator.

From the listed tests, we will analyze the Widget tests. The goal is to test the widget since everything is a widget in a Flutter.

Tests must be:

  • independent;
  • should not create widgets;
  • be simple, readable, and fast;

For test widgets, the testWidgets method is presented with the main parameters description, where you can write a description in string format, and the next parameter class WidgetTester is a class that programmatically interacts with widgets and the test environment.

testWidgets('Test description', (WidgetTester widgetTester) async {  
  //...
});

Take, for example, the code below, with an input field and Sing In button:

class SignInScreen extends StatefulWidget {  
  const SignInScreen({super.key});  
  
  @override  
  State<StatefulWidget> createState() => _SignInScreenState();  
}  
  
class _SignInScreenState extends State<SignInScreen> {  
  
  final TextEditingController _textEditingController = TextEditingController();  
  
  @override  
  Widget build(BuildContext context) {  
    return Scaffold(  
      appBar: AppBar(title: const Text('Check email'),),  
      body: Padding(  
        padding: const EdgeInsets.all(8.0),  
        child: Column(  
          children: [  
            TextField(  
              controller: _textEditingController,  
              key: const ValueKey('email_field'),  
              decoration: const InputDecoration(hintText: 'Enter e-mail'),  
            ),  
            const SizedBox(height: 24,),  
            ElevatedButton(onPressed: (){  
              Navigator.push(context, MaterialPageRoute(builder: (context) => const SuccessScreen()));  
            }, child: const Text('Sing In'))  
          ],  
        ),  
      ),  
    );  
  }  
  
}

class MyApp extends StatelessWidget {  
  const MyApp({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return MaterialApp(  
      title: 'Flutter Widget test',  
      theme: ThemeData(  
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),  
        useMaterial3: true,  
      ),  
      home: const SignInScreen(),  
    );  
  }  
}

And let's write a widget test:

void main() {  
  testWidgets('Widget test', (WidgetTester widgetTester) async {  
    await widgetTester.pumpWidget(const MaterialApp(home: SignInScreen(),)); 
     
    Finder title = find.text('Check email');  
    expect(title, findsOneWidget);  
  });  
  testWidgets('Test input field', (WidgetTester widgetTester) async {  
    await widgetTester.pumpWidget(const MaterialApp(home: SignInScreen(),)); 
     
    Finder emailTextField = find.byKey(const ValueKey('email_field'));  
    expect(emailTextField, findsOneWidget);  
  });  
  testWidgets('Test sign in button', (WidgetTester widgetTester) async {  
    await widgetTester.pumpWidget(const MaterialApp(home: SignInScreen(),));
      
    Finder signInButton = find.byType(ElevatedButton);    
    expect(signInButton, findsOneWidget);  
  });  
}

What the pumpWidget method does is render the passed widget. Next, using Finder we look for the widget we need. You can search in different ways and using different finders. Here are the most common ones (there are many more). You can also easily write your finder.

  • find.text() - searches for text;

  • find.byKey() - searches for a widget by key;

  • find.byType() - searches for a widget by type;

  • find.byIcon() - searches for a widget of type Icon;

  • find.byWidgetPredicate() - searches for a widget by predicate;

In the example above, I use three methods for the title using the text find.text('Check email'), for the input field using the key find.byKey(const ValueKey('email_field')), and for the button using the type find.byType( ElevatedButton).

All that remains is to compare the resulting result with the given finders using the Matcher class. There are also a lot of them written for almost every occasion in life. But you can easily write your own.

Below are the most, in my opinion, used:

  • findsOneWidget - compares that Finder finds only one widget;

  • findsWidgets - compares that Finder finds at least one widget;

  • isSameColorAs(Color color) - compares that an object has a certain color;

  • findsNothing - compares that Finder does not find the widget;

  • isNotNull - compares that the object is not null;

When we run the test and there is an error, we get the following result:

Here, I deliberately omitted a letter to show an error.

If the test is passed successfully, we get the following result:

Thank you for your attention!


Written by azamatnurkhojayev | I'm an Android developer. Teaches courses at the programming school, and writes articles about development.
Published by HackerNoon on 2024/01/12