One, foreword
The testing of mobile applications is often complicated and involves a lot of work. In order to verify the real user experience, manual testing across multiple platforms and different physical devices is often required. As product functionality is iterated over time, the complexity of testing increases dramatically, making manual testing more difficult. Therefore, writing automated test cases plays a very important role in updating and iterating our projects.
Unit testing
Unit testing is a way of verifying the smallest testable units in software, using unit tests to verify the behavior of individual functions, methods, or classes. Let’s take a look at the Project catalog for the Flutter project:
As shown in the figure above, lib is the source file directory of the Flutter application and test is the test file directory. Let’s look at the steps for writing unit test cases.
2.1 Related Steps
2.1.1 Adding a Dependency
The Flutter project adds flutter_test package by default. If the dart package does not rely on Flutter, you can import the test package as follows:
dev_dependencies:
flutter_test:
sdk: flutter
//or
test:
Copy the code
2.1.2 Declare a test class
Create a dart file in the lib directory and declare a class to test with the following code:
//unit.dart
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
Copy the code
2.1.3 Writing Test Cases
Create a DART file in the test directory (the recommended file name ends with _test) and write test cases. Test cases typically contain steps for definition, execution, and validation, as shown in the following example:
//unit_test.dart
import 'package:flutter_unit_test/unit.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// Check whether the Counter object is equal to 1 after calling increase
test('Increase a counter value should be 1', () {
final counter = Counter();
counter.increase();
expect(counter.count, 1);
});
// The second use case checks whether 1+1 equals 2
test('1+1 should be 2', () {
expect(1 + 1.2);
});
}
Copy the code
You can see that validation requires the use of the Expect function to compare the results of the execution of the smallest measurable unit to expectations. Additionally, test cases need to be wrapped inside test(), which is the test case wrapper class provided by FLUTTER.
2.1.4 Starting test Cases
Dart file and right-click “Run ‘tests in widget_test'” from the menu to launch the test case. The running results are as follows:
Next we modify the test case code as follows:
void main() {
// Check whether the Counter object is equal to 1 after calling increase
test('Increase a counter value should be 1', () {
final counter = Counter();
counter.increase();
expect(counter.count, 2);
});
// The second use case checks whether 1+1 equals 2
test('1+1 should be 2', () {
expect(1 + 1.2);
});
}
Copy the code
As you can see, we changed the first use case from 1 to 2 to create an error. Now let’s see if the test case fails:
2.1.5 Combining Test Cases
If you have multiple test cases that are associated with each other, you can use the group function on the outer layer to group them together, with the following example code:
void main() {
// Combine test cases to determine if Counter is equal to 1 after calling increase,
// And check whether the Counter object is waiting after calling the Decrease method
group('Counter', () {
test('Increase a counter value should be 1', () {
final counter = Counter();
counter.increase();
expect(counter.count, 1);
});
test('Decrease a counter value should be -1', () {
final counter = Counter();
counter.decrease();
expect(counter.count, - 1);
});
});
}
Copy the code
In addition to the above startup methods, you can also use terminal commands to start test cases, as shown in the following example:
// Flutter test file path
flutter test test/unit_test.dart
It is also possible to run the flutter run file path onto a real machine or emulator
Copy the code
2.2 Use Mockito to simulate external dependencies
We may also need to get data from external dependencies (such as web services) for unit testing. Let’s start with an example of creating a class to test in lib:
//mock.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class Todo {
final String title;
Todo({this.title});
// Factory class constructor to convert JSON to objects
factory Todo.fromJson(Map<String.dynamic> json) {
return Todo(
title: json['title']); } } Future<Todo> fetchTodo(http.Client client)async {
// Get network data
final response = await client.get('https://xxx.com/todos/1');
if (response.statusCode == 200) {
// Request successful, parse JSON
return Todo.fromJson(json.decode(response.body));
} else {
// The request failed, an exception was thrown
throw Exception('Failed to load post'); }}Copy the code
As you can see, the data interaction with the Web service is beyond the control of our program, and it is difficult to cover all possible success or failure use cases, so it is better to simulate these “external dependencies” in test cases that can return specific content. Let’s look at the steps to simulate external dependencies using Mockito:
2.2.1 Adding a Dependency
Add the mockito package to dev_dependencies in the pubspec.yaml file:
dependencies:
http: ^0.122.
dev_dependencies:
flutter_test:
sdk: flutter
mockito:
Copy the code
2.2.2 Creating mock Classes
Create a mock class as shown below:
//mock_test.dart
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
class MockClient extends Mock implements http.Client {}
Copy the code
As you can see, we defined a mock class MockClient that gets the external interface of http.Client in the form of an interface declaration.
2.2.3 Writing Test cases
Now we can use the WHEN statement to inject MockClient and return the corresponding data when it calls the Web service as follows:
//mock_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_unit_test/mock.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
class MockClient extends Mock implements http.Client {}
void main() {
group('fetchTodo', () {
test('returns a Todo if successful', () async {
final client = MockClient();
// Use Mockito to inject the JSON field of the successful request
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('{"title": "Test"}'.200));
// Verify that the request result is a Todo instance
expect(await fetchTodo(client), isInstanceOf<Todo>());
});
test('throws an exception if error', () {
final client = MockClient();
// Use Mockito to inject Error for request failure
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('Forbidden'.403));
// Verify that the result of the request throws an exception
expect(fetchTodo(client), throwsException);
});
});
}
Copy the code
You can see that in the first case we injected json results, and in the second case we injected a 403 exception. Let’s take a look at the results:
As you can see, our program successfully simulates both normal and abnormal conditions without calling the real Web service.
So much for the unit test of Flutter. Careful students may notice that the whole Module of Flutter unit test is very similar to Android.
3. UI automation test
3.1 Simple Example
To test the widget class, we need to use the flutter _test package. Take a Flutter default timer application template as an example:
Its UI test case could be written like this:
//widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_unit_test/widget.dart';
void main() {
testWidgets('Counter increments UI test', (WidgetTester tester) async {
// Declare the Widget object you want to validate (i.e., MyApp) and trigger its rendering
await tester.pumpWidget(MyApp());
// Find the Widget with the string text '0' to verify that the find was successful
expect(find.text('0'), findsOneWidget);
// Failed to find Widget with string text '1'
expect(find.text('1'), findsNothing);
// Find the '+' button and apply the click action
await tester.tap(find.byIcon(Icons.add));
// Triggers its rendering
await tester.pump();
// Failed to find the Widget with string text '0'
expect(find.text('0'), findsNothing);
// Find the Widget with string text '1' to verify that the find is successful
expect(find.text('1'), findsOneWidget);
});
}
Copy the code
Right click on the file and select the Run ‘tests in widget_test.dart’ option to execute the test. The test results are as follows:
3.2 Related steps and API details
The Flutter_test package provides the following tools for testing widgets:
- TestWidgets () : This function automatically creates a WidgetTester for each test, in place of the normal test function.
- WidgetTester: Use this class to build widgets and interact with them in a test environment.
- Finder: This class makes it easy to find widgets in our test environment.
- Mathcer constant: This constant helps us verify that the Finder has located one or more widgets in our test environment.
Let’s look at the steps involved in writing a test case:
3.2.1 Adding the Flutter_test dependency
Add flutter_test dependencies to dev_dependencies in pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
Copy the code
3.2.2 Creating widgets for testing
Take the default Flutter timer application template as an example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page')); }}class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.); }}Copy the code
3.2.3 Create a testWidgets test method
Define a test with the testWidgets() function provided with the Flutter_test package. The testWidgets function defines a widget test and creates a WidgetTester that can be used.
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments UI test', (WidgetTester tester) async{}); }Copy the code
3.2.4 Build and render widgets using WidgetTester
In the previous step, we created a WidgetTester that allows you to create, render, and interact with widgets in a test environment. Let’s take a look at some common apis in WidgetTester.
Create/render class API
- PumpWidget (Widget Widget) : Create and render the widgets we provide.
- Pump (Duration Duration) : Triggers widget rebuild. Unlike the pumpWidget, the pumpWidget forces a complete tree rebuild even if the widget is the same as the previous call, whereas Pump will only rebuild the changed widget. For example, if we click on the button that calls setState(), we can use the pump method to make flutter build our widget again.
- PumpAndSettle () : Call pump() repeatedly for a given period until all frames are drawn, usually until all animations are complete.
Interactive class API
- EnterText () : emulated input text.
- Tap () : Simulates clicking a button.
- Drag () : Simulates sliding.
- LongPress () : simulates longPress.
The other methods are not covered here, but if you want to learn more about them, use WidgetTester.
3.2.5 Use Finder to locate widgets
In our test environment, to locate the widget, we need the Finder class.
- Text (String text) : Finds widgets that contain specific text, such as find.text(‘0’).
- WidgetWithText () : Specifies the type of widget that contains the given text, such as find.widgetwithText (Button, ‘0’).
- ByKey (Key Key) : uses a specific Key to find widgets. For example, find byKey (Key (‘ H ‘)).
- ByType (Type Type) : Finds the widget based on Type. The Type parameter must be a subclass of the widget, for example, find.byType(IconButton).
- ByWidget (Widget Widget) : Find the corresponding Widget based on the Widget instance. The following is an example:
Widget myButton = new Button(
child: new Text('Update')); find.byWidget(myButton);Copy the code
- ByWidgetPredicate () : Matches the widget based on its properties, as shown in the following example:
find.byWidgetPredicate(
(Widget widget) => widget is Tooltip && widget.message == 'Back',
description: 'widget with tooltip "Back"'.)Copy the code
If you want to learn more about these topics, check out CommonFinders.
3.2.6 Use Matcher constants for validation
Flutter_test provides the following matchers:
- FindsOneWidget: Find a widget
- FindsWidgets: Find one or more widgets
- FindsNothing: No widget found
- FindsNWidgets: Finds the specified number of widgets
Such as:
// Failed to find the Widget with string text '0'
expect(find.text('0'), findsNothing);
Copy the code
Now that we have some understanding of widget testing, let’s take a look at the widget test case we wrote above to get a better understanding:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_unit_test/widget.dart';
void main() {
testWidgets('Counter increments UI test', (WidgetTester tester) async {
// Declare the Widget object you want to validate (i.e., MyApp) and trigger its rendering
await tester.pumpWidget(MyApp());
// Find the Widget with the string text '0' to verify that the find was successful
expect(find.text('0'), findsOneWidget);
// Failed to find Widget with string text '1'
expect(find.text('1'), findsNothing);
// Find the '+' button and apply the click action
await tester.tap(find.byIcon(Icons.add));
// Triggers its rendering
await tester.pump();
// Failed to find the Widget with string text '0'
expect(find.text('0'), findsNothing);
// Find the Widget with string text '1' to verify that the find is successful
expect(find.text('1'), findsOneWidget);
});
}
Copy the code
Although widget testing expands the scope of application testing to find problems that cannot be found by unit testing, widget test cases are very expensive to develop and maintain compared to unit testing. Therefore, it is recommended that after a project reaches a certain size and business characteristics have a certain continuity rule, Consider the need for widget testing.
For more exciting content, please pay attention to Tencent VTeam technical team wechat public account and video number
Original author: Dan Chao
Without consent, prohibit reprint!