Flutter has gained immense popularity for its capability to build cross-platform apps from a single codebase. A crucial part of Flutter app development is Flutter testing, which ensures that your application performs as expected. In this blog, we’ll delve into the three primary types of testing in Flutter: Unit testing in Flutter, Widget testing in Flutter, and Integration testing in Flutter, and explore how to write each effectively.
Why Testing is Important in Flutter
Flutter test automation plays a key role in ensuring smooth development cycles. Testing helps identify bugs early, enhances code maintainability, and improves user experience. By implementing robust Flutter test cases, developers can:
- Prevent regressions
- Reduce manual testing efforts
- Ensure smoother app performance and reliability
Flutter offers a rich set of tools and libraries for Flutter test framework integration, making it easier to maintain a high standard of code quality.
Types of Flutter Tests
Flutter Unit Testing:
- Unit tests verify individual functions, methods, or classes in isolation. Example: Testing a simple Cart function.
Flutter Integration Testing:
- Integration tests validate the complete app or a large part of it with actual UI interactions. Example: Testing a Login screen.
1. What is Flutter Unit Testing?
Unit testing in Flutter verifies individual units of code (typically methods or functions) to ensure they work as expected. It is the smallest form of testing and helps catch bugs early in development.
Setting Up a Flutter Unit Test
Before writing tests, add the Flutter testing dependency to your pubspec.yaml file:
Then, run flutter pub get
to fetch the package.
Writing Your First Unit Test
Let’s assume we have a simple CartItem
class:
class CartItem {
final String name;
final double price;
final int quantity;
CartItem({required this.name, required this.price, required this.quantity});
double get total => price * quantity;
}
class ShoppingCart {
final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
void addItem(CartItem item) {
_items.add(item);
}
void removeItem(String name) {
_items.removeWhere((item) => item.name == name);
}
double calculateTotal() {
return _items.fold(0.0, (total, item) => total + item.total);
}
void clearCart() {
_items.clear();
}
}
Code language: JavaScript (javascript)
To test this class, create a new file cart_item_test.dart
under test/:
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttertest/cart_item.dart';
void main() {
late ShoppingCart cart;
setUp(() {
cart = ShoppingCart();
});
test('Add item to cart', () {
final item = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
cart.addItem(item);
expect(cart.items.length, 1);
expect(cart.items.first.name, 'Laptop');
expect(cart.items.first.price, 1000.0);
expect(cart.items.first.quantity, 1);
});
test('Remove item from cart', () {
final item = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
cart.addItem(item);
cart.removeItem('Laptop');
expect(cart.items.isEmpty, true);
});
test('Calculate total price of items in cart', () {
final item1 = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
final item2 = CartItem(name: 'Phone', price: 500.0, quantity: 2);
cart.addItem(item1);
cart.addItem(item2);
expect(cart.calculateTotal(), 2000.0);
});
test('Clear cart', () {
final item1 = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
final item2 = CartItem(name: 'Phone', price: 500.0, quantity: 2);
cart.addItem(item1);
cart.addItem(item2);
cart.clearCart();
expect(cart.items.isEmpty, true);
});
}
Key Components of Flutter Unit Tests
-
test():
Defines a Flutter test case. -
expect():
Compares actual output with expected output.
Explanation
Test 1 – Add Item to Cart
_Purpose: _This test ensures that the addItem()
method functions correctly and that the cart is updated when a new item is added.
Steps:
- Create a
CartItem
named “Laptop” with a price of 1000.0 and quantity of 1. - Call
cart.addItem(item)
to add the item to the shopping cart. Use
expect()
to assert that the cart:Contains 1 item
(cart.items.length)
The first item in the cart has the name “Laptop”
The price of the first item is 1000.0
The quantity of the first item is 1
Test 2 – Remove Item from Cart
Purpose: This test ensures that the removeItem() method removes the correct item from the cart.
Steps:
- Create a
CartItem
named “Laptop” and add it to the cart. - Call
cart.removeItem('Laptop')
to remove the item. - Use
expect()
to assert that the cart is empty (cart.items.isEmpty
is true), confirming the item was successfully removed.
Test 3 – Calculate Total Price of Items in Cart
Purpose: This test checks if the calculateTotal()
method correctly calculates the total price for all items in the cart.
Steps:
- Create two items: “Laptop” (price 1000.0, quantity 1) and “Phone” (price 500.0, quantity 2).
- Add both items to the cart using
cart.addItem()
. - Call
cart.calculateTotal()
to get the total price. - Use
expect()
to assert that the total price is 2000.0 (since 1000 + 500*2 = 2000).
Test 4 – Clear Cart
Purpose: This test ensures that the clearCart()
method successfully removes all items from the cart.
Steps:
- Create two items: “Laptop” and “Phone”, and add them to the cart.
- Call
cart.clearCart()
to remove all items. - Use
expect()
to assert that the cart is empty (cart.items.isEmpty
is true).
By testing each of these functions, we ensure that the cart behaves as expected, handling typical use cases like adding, removing, and totaling items, as well as clearing the cart. Unit tests like these help catch potential issues early, ensuring robust and reliable business logic.
These tests are important for any real-world application, especially e-commerce apps where accuracy in cart operations directly impacts user experience and functionality.
Run Tests and result:
Command – flutter test test/cart_item_test.dart
Result:
2. What is Flutter Widget Testing?
Flutter widget testing is used to test individual UI components (widgets) in isolation, ensuring they behave as expected. These tests are one level above unit testing in Flutter and focus on interactions, rendering, and layout of the widget tree without requiring a full app running environment or external services.
Let’s take an example of a contact screen:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class ContactScreen extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ContactForm(),
);
}
}
class ContactForm extends StatefulWidget {
const ContactForm({super.key});
@override
_ContactFormState createState() => _ContactFormState();
}
class _ContactFormState extends State<ContactForm> {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _messageController = TextEditingController();
void _submitForm() {
// Simply show a snackbar upon submission
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Form Submitted')));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Contact Form')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
),
TextField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
TextField(
controller: _messageController,
decoration: const InputDecoration(labelText: 'Message'),
maxLines: 3,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
child: const Text('Submit'),
),
],
),
),
);
}
}
Writing the Widget Test
Create a contact_screen_test.dart
in your test/ directory.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttertest/contact_screen.dart';
void main() {
testWidgets('Contact Form UI - Submit button test',
(WidgetTester tester) async {
// Build the ContactForm widget
await tester.pumpWidget(const MaterialApp(home: ContactForm()));
// Find the text fields and submit button
final nameField = find.byType(TextField).first;
final emailField = find.byType(TextField).at(1);
final messageField = find.byType(TextField).at(2);
final submitButton = find.text('Submit');
// Verify if the text fields are present
expect(nameField, findsOneWidget);
expect(emailField, findsOneWidget);
expect(messageField, findsOneWidget);
// Verify if the submit button is present
expect(submitButton, findsOneWidget);
// Enter text into the fields
await tester.enterText(nameField, 'John Doe');
await tester.enterText(emailField, 'john.doe@example.com');
await tester.enterText(
messageField, 'Hello! I would like to inquire about...');
// Tap the submit button
await tester.tap(submitButton);
await tester.pump();
});
}
Explanation
- Find Widgets: Use
find.byKey
,find.text
, orfind.byType
to locate UI elements. - Interact with Widgets: Use
enterText
,tap
, etc. - Rebuild UI:
await tester.pump()
simulates a frame being drawn after a state change. - Expectations: Check UI outputs using
expect
.
Run the Tests:
Use the following command in your terminal to run the widget test:
flutter test test/contact_screen_test.dart
This will execute the test cases and provide a report of the results.
Result:
Explanation:
Building the Widget:
await tester.pumpWidget(MaterialApp(home: ContactForm()));
initializes the ContactForm widget inside aMaterialApp
.Finding Widgets:
-
find.byType(TextField)
is used to locate the threeTextField
widgets (for name, email, and message). -
find.text('Submit')
locates the submit button. Verifying Widget Presence:
expect(nameField, findsOneWidget)
checks if the text fields are rendered on the screen.expect(submitButton, findsOneWidget)
checks if the submit button is rendered.Simulating User Input:
await tester.enterText(nameField, 'John Doe');
simulates typing into the name field.Similarly for the email and message fields.
Tapping the Button:
await tester.tap(submitButton);
simulates a tap on the submit button.await tester.pump();
triggers a re-render to reflect the updated state after the tap.
3. What is Flutter Integration Testing?
Flutter integration testing verifies the entire app’s functionality by testing multiple widgets, screens, and external dependencies working together. These tests simulate real-world usage and are more complex, as they often involve interacting with the real UI, API calls, and databases.
To set up integration tests:
Flutter has a package specifically for integration testing: integration_test
. You can also run these tests on an actual device or emulator.
Add the Required Dependencies
To get started, you need to add the integration_test
and flutter_test
packages to your pubspec.yaml
file. If you’re working with Firebase or other back-end services, you might also need to include specific dependencies for mocking or interacting with them.
Write an Example Integration Test
Now let’s write a simple integration test for the above login screen.
Below integration test will cover all the following scenarios:
- Successful login: When the correct email and password are entered, the login should be successful.
- Failed login due to incorrect credentials: When the wrong email or password is entered, the login should fail.
- Empty fields: Ensure that both fields are validated (even though your code does not explicitly validate, we can test that pressing the button with empty fields results in failure).
- UI behavior: Check that the UI components are displayed correctly, such as the
TextField
widgets andElevatedButton
. You can place this test in yourintegration_test
directory.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttertest/login_screen.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('LoginScreen Integration Test', () {
testWidgets('Successful login scenario', (tester) async {
// Pump the LoginScreen widget into the widget tree
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Enter correct credentials
await tester.enterText(
find.byKey(const Key('emailField')), 'test@example.com');
await tester.enterText(
find.byKey(const Key('passwordField')), 'password123');
// Tap the login button
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
// Verify the "Login Successful" message is shown
expect(find.text('Login Successful'), findsOneWidget);
});
testWidgets('Failed login with incorrect credentials', (tester) async {
// Pump the LoginScreen widget into the widget tree
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Enter incorrect credentials
await tester.enterText(
find.byKey(const Key('emailField')), 'wrong@example.com');
await tester.enterText(
find.byKey(const Key('passwordField')), 'wrongpassword');
// Tap the login button
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
// Verify the "Login Failed" message is shown
expect(find.text('Login Failed'), findsOneWidget);
});
testWidgets('Login attempt with empty fields', (tester) async {
// Pump the LoginScreen widget into the widget tree
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Leave both fields empty
await tester.enterText(find.byKey(const Key('emailField')), '');
await tester.enterText(find.byKey(const Key('passwordField')), '');
// Tap the login button
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
// Verify the "Login Failed" message is shown (since we expect no credentials entered)
expect(find.text('Login Failed'), findsOneWidget);
});
testWidgets('UI elements are present', (tester) async {
// Pump the LoginScreen widget into the widget tree
await tester.pumpWidget(MaterialApp(home: LoginScreen()));
// Verify the email and password fields are present
expect(find.byKey(const Key('emailField')), findsOneWidget);
expect(find.byKey(const Key('passwordField')), findsOneWidget);
// Verify the login button is present
expect(find.byKey(const Key('loginButton')), findsOneWidget);
});
});
}
Explanation:
- Successful login:
The test enters the correct email and password (test@example.com
and password123
), taps the login button, and expects the SnackBar message “Login Successful”.
- Failed login due to incorrect credentials:
This test enters incorrect credentials (wrong@example.com
and wrongpassword
) and expects the “Login Failed” message in the SnackBar
.
- Empty fields:
This test verifies that if the email and password fields are left empty, tapping the login button results in the “Login Failed” message. This assumes the empty field case results in failure, as your code does not have validation for empty fields, but it could be something you’d like to add.
- UI behavior:
This test checks that the key elements (the email field, password field, and login button) are properly displayed on the screen.
Running the Integration Test:
Command – flutter test integration_test/login_screen_test.dart
Result:
Conclusion
Flutter automated testing is a vital aspect of building reliable and maintainable Flutter applications. By leveraging unit testing in Flutter, widget testing, and Flutter integration tests, you can ensure that each piece of your app functions as expected and integrates seamlessly with other components. Flutter’s powerful testing tools make it easier than ever to implement a comprehensive testing strategy for your apps.
To download the source code for the sample app, click here.
Source Link : Flutter Testing: Unit, Widget & Integration Tests Guide
Top comments (0)