Hello everybody, today we're gonna talk about tests with flutter and the importance of doing that in your projects, so let's get started.
Why do we need to test?
As our flutter project grows the importance of testing it grows too, so since your code was well made using good patterns and following the best practices, it makes easier to implement a test to grant you the app works as you've projected for.
Concept
There are some concepts of how to write tests and provide better and well-projected apps, one of the most common is the TDD (Test Driven Development), which says that you have to write your test first and then write the code, and repeat the process of how many times you need to ensure that everything is passing in test and the code only does what it has to do.
Types of test
With Flutter we have three types of tests, they are:
Unit test: as the name said, this kind of test has the purpose of checking if the minimum part of your code is working as expected, this includes for example functions and classes.
Widget test: This one verifies if the widgets are working as expected, so the purpose of this kind of test is to check the UI consistency.
Integration test: The last one, the integration test, it's personally my favorite type, this is like a mix of the two types of the test described before, with this you can check an entire flow, so you can verify if the checkout of an e-commerce app is working as expected for example.
With all that said let's dive inside some examples to understand a little bit better. In this case, I will show how to test a login because it is a common feature for most of the projects.
To get started, suppose that you've already the flutter in your O.S then just create a project with the command flutter create login_test
where the name login_test
can be changed if you wanna give another name for your project, with this created verify in your root directory and you be able to find a folder called test
, all of your testing code should be inside that and by default, all the classes created for this purpose have to be in the the _test.dart
to let the language recognize this as a test class.
To get started using the TDD concept, let's create the unit test first before the implementation.
Unit test
delete the file that comes with the project and create a file called unit_login_test.dart
in test
folder with the following code:
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Login controller', () {
test("success login", () {
LoginController controller = LoginController();
expect(controller.login("username", "password"), true);
});
test("failed login", () {
LoginController controller = LoginController();
expect(controller.login("invalidUsername", "invalidPassword"), false);
});
});
}
We create a main function, which starts in this case these tests, we have to import the class flutter_test
to execute as we want, to do this we create a group
and inside put all our test cases, initiate our class and execute the function with some given parameters, and the expected return in expect test function to do that we imagine that we have a controller and inside a login function that receives the username and the password and if match returns a boolean. To execute now we have to create a file called login_controller.dart
inside the lib
folder with the following code:
class LoginController {
bool login(String username, String password) {
return username == "username" && password == "password";
}
}
With that, we only have to import this class inside our test class and execute it to see the result
import 'package:login_test/login_controller.dart';
Now let's move to the second type of test
Widget test
Like the unit test, we will create the test file and implement that, to get started create a file called widget_login_test.dart
and put the following code:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("Login UI", (tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginPage()));
final username = find.byKey(const Key("usernameTextField"));
final password = find.byKey(const Key("passwordTextField"));
final loginButton = find.byKey(const Key("loginButton"));
expect(username, findsOneWidget);
expect(password, findsOneWidget);
expect(loginButton, findsOneWidget);
await tester.enterText(username, "username");
await tester.enterText(password, "password");
await tester.tap(loginButton);
await tester.pump();
final successMessage = find.byKey(const Key('success'));
expect(successMessage, findsOneWidget);
});
}
The widget test renders the widgets and you can perform actions like tap()
and enterText()
to interact with the components, in this test case we need to search using the Key parameter, you can do it using a text, but the Key gives you more confidence about getting the right thing. Now to implement the code just create a file called login_page.dart
and put this code below:
import 'package:flutter/material.dart';
import 'package:login_test/login_controller.dart';
LoginController loginController = LoginController();
TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: usernameController,
key: const Key('usernameTextField'),
),
TextField(
controller: passwordController,
key: const Key('passwordTextField'),
),
const SizedBox(height: 16),
ElevatedButton(
key: const Key('loginButton'),
child: const Text("Login"),
onPressed: () {
// Example login logic
if (loginController.login(
usernameController.text, passwordController.text)) {
showDialog(
context: context,
builder: (_) => const AlertDialog(
key: Key('success'),
title: Text("Login Successful"),
),
);
}
},
),
],
),
);
}
}
Now we only need to import the LoginPage
in our widget test file, just type import 'package:login_test/login_page.dart';
with that you can execute your test and should see something like
Integration test
This is the last type of test, it works by executing a complete feature, to do that you need a real device, to start we need to add a new dependency in our file pubspec.yaml
in the section dev_dependencies
called integration_test: sdk: flutter
, after this just type flutter pub get
to update the dependencies, with this set up complete you need to create a folder called integration_test
in the root directory, then insert a file who you can call app_test.dart
with the code below:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:login_test/login_page.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
group('LoginPage', () {
testWidgets("Login UI", (tester) async {
await tester.pumpWidget(const MaterialApp(home: LoginPage()));
final username = find.byKey(const Key("usernameTextField"));
final password = find.byKey(const Key("passwordTextField"));
final loginButton = find.byKey(const Key("loginButton"));
expect(username, findsOneWidget);
expect(password, findsOneWidget);
expect(loginButton, findsOneWidget);
await tester.enterText(username, "username");
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.enterText(password, "password");
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(loginButton);
await tester.pumpAndSettle(const Duration(seconds: 1));
final successMessage = find.byKey(const Key('success'));
expect(successMessage, findsOneWidget);
});
});
}
this code is very similar to the widget test, but we have a few differences, we use IntegrationTestWidgetsFlutterBinding.ensureInitialized();
and WidgetsFlutterBinding.ensureInitialized();
to ensure that we have the integration test and the widgets ready to start the process.
Bonus
With an additional part of this article, we gonna see how to get a visual report of the test coverage, if you don't wanna learn this just skip to the conclusion.
to do that we have to run in terminal flutter test --coverage
, after executing this should create a folder called coverage with a file named lcov.info inside as we see below:
after that you have to convert this file into a bunch of files to be read by a web server, to continue just type in your terminal genhtml coverage/lcov.info -o coverage/html
where the coverage/lcov.info
is the path of the file created above and the coverage/html
after the flag -o is the directory where I want to save the generated files when this part finish, you will see a folder with the given name created where you choose.
to finish we have to expose this directory to access in a web browser, to do that I recommend getting the dhttpd
library from here just type in your terminal dart pub global activate dhttpd
with this you've downloaded the library globally, after this just type dhttpd --path coverage/html/
and open a browser to put the URL http://localhost:8080
and finally you will be able to see a full report about your test like you see bellow:
With this we finish the bonus part, this could be essential to understand where the code is not covered and where the tests are failing.
Conclusion
As we see, the tests give us more confidence in the final result focus on the release, this process can take more time to finish a feature, but the gain with that is more valuable than if a bug was reported after distribute of the project, so hope everybody understands how to do and the importance, hope to see you next time.
If you wanna see the full code, i've uploaded the code, the link is: Repository
Top comments (0)