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.12.2
 
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:

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.