The official documentation

When there are more and more functions in the App, it will become very troublesome for us to test a function manually. At this time, we need unit test to help us test the function we want to test.

Three tests are provided in Flutter:

  • Unit test: unit test
  • Widget test: Widget test
  • Integration test: Integration test

The first two are recorded here.

When a new Flutter project is created, there will be a test directory under the project directory to store the test files:

Unit testing

Unit tests verify that a method or a piece of logic in your code is correct. The steps for writing unit tests are as follows:

  1. Add test or flutter_test dependencies to the project
  2. intestCreate a test file under:counter_test.dart
  3. Create a file to test as follows:counter.dart
  4. incounter_test.dartWritten in filetest
  5. This can be used when multiple tests need to be tested togethergroup
  6. Run test classes

1. Add dependencies

Add flutter_test dependencies to project pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter
Copy the code

2. Create a test file

Dart file and counter. Dart file to be tested. When the two files are created, the directory structure looks like this:

.├ ── lib │ ├─ Countertest│ ├ ─ ─ counter_test. DartCopy the code

3. Write the tested class

The methods in the Counter class are as follows:

class Counter {
  int value = 0;

  void increment() => value++;

  void decrement() => value--;
}
Copy the code

4. Write test classes

Write unit tests in the counter_test.dart file that use the top-level methods provided by the Flutter_test package, such as test(…) Methods are used to define a unit test, and expect(…) Method used to verify the results.

test(…) The first parameter is the description of the unit test, and the second is a Function to write the test.

expect(…) There are also two required parameters in the method, the first being the variable to validate and the second being the value that matches that variable.

The code in counter_test.dart is as follows:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/counter.dart';

Dart can also be run with the flutter test test/counter_test.dart command

void main() {
  // Single test
  test("Test value increment", () {
    final counter = Counter();
    counter.increment();
    
    // Verify that counter. Value is 1
    expect(counter.value, 1);
  });
Copy the code

5. Use groups to perform multiple tests

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/counter.dart';

void main() {
  // Use group to merge multiple tests. Tests multiple associated tests
  group("Counter", () {
    test("value should start at 0", () {
      expect(Counter().value, 0);
    });

    test("value should be increment", () {
      final counter = Counter();

      counter.increment();

      expect(counter.value, 1);
    });

    test("value should be decremented", () {
      final counter = Counter();

      counter.decrement();

      expect(counter.value, - 1);
    });
  });
}
Copy the code

6. Perform unit tests

If you are using Android Studio or Idea, click the Run button on the side to execute or debug:

If you are using VSCode, you can use the command to perform the test:

flutter test test/counter_test.dart
Copy the code

Network interface test

Dart to request an interface in the test directory and verify the result:

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;

void main() {
  test("Test network Request", () async {
    // If the request requires a token
    final token = "54321";
    final response = await http.get(
      "https://api.myjson.com/bins/18mjgh",
      headers: {"token": token},
    );
    if (response.statusCode == 200) {
      // Validate the token in the request header
      expect(response.request.headers['token'], token);
      print(response.request.headers['token']);
      print(response.body);
      // Parse the returned JSON
      Person person = parsePersonJson(response.body);
      Verify that the person object is not empty
      expect(person, isNotNull);
      // Check that the property values in the person object are correct
      expect(person.name, "Lili");
      expect(person.age, 20);
      expect(person.country, 'China'); }}); }Copy the code

useMockitoTo simulate object dependencies

First, add mockito’s dependency to pubspec.yaml:

dev_dependencies:
  mockito: 4.11.
Copy the code

Then create a new class to be tested:

class A {
  int calculate(B b) {
    int randomNum = b.getRandomNum();
    return randomNum * 2; }}class B {
  int getRandomNum() {
    return Random().nextInt(100); }}Copy the code

In the code above, the calculate method of class A depends on class B. Use mockito to simulate a class B when testing the Calculate method

Then create a new test class:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/mock_d.dart';
import 'package:mockito/mockito.dart';

// use mockito to simulate a class B
class MockB extends Mock implements B {}

void main() {
  test("Test using mockito to mock dependencies", () {
    var b = MockB();
    var a = A();
    // Returns 10 when the b.getrandomnum () method is called
    when(b.getRandomNum()).thenReturn(10);
    expect(a.calculate(b), 20);

    / / check b.g etRandomNum (); Whether it has been called
    verify(b.getRandomNum());
  });
}
Copy the code

There is an example of using mockito to simulate the data returned by the interface as follows:

Future<Post> fetchPost(http.Client client) async {
  final response =
      await client.get("https://jsonplaceholder.typicode.com/posts/1");
  if (response.statusCode == 200) {
    return Post.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed to load post'); }}Copy the code

In the above method, an interface is requested and parsed back if the request succeeds, otherwise an exception is thrown. The code to test this method is as follows:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/post_service.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';

/// Mock an http.client object
class MockClient extends Mock implements http.Client {}

void main() {
  group("fetchPost", () {
    test("Interface returns correct data", () async {
      final client = MockClient();

      // Returns the specified data when the specified interface is called
      when(client.get("https://jsonplaceholder.typicode.com/posts/1"))
          .thenAnswer((_) async {
        return http.Response(
            '{"title": "test title", "body": "test body"}'.200);
      });
      var post = await fetchPost(client);
      expect(post.title, "test title");
    });

    test("Interface returns data error, throws exception", () {
      final client = MockClient();

      // Return Not Found when this interface is called
      when(client.get("https://jsonplaceholder.typicode.com/posts/1"))
          .thenAnswer((_) async {
        return http.Response('Not Found'.404);
      });
      expect(fetchPost(client), throwsException);
    });
  });
}
Copy the code

The Widget test

One obvious difference between Widget testing and unit testing is that the top-level function used by Widget testing is testWidgets, which is written as follows:

testWidgets('This is a Widget test', (WidgetTester tester){

});
Copy the code

We can use WidgetTester to build widgets that need to be tested, or perform a redraw (equivalent to calling setState(…)). Methods.

Another top-level function, find, can be used to locate widgets that need to be manipulated, such as:

find.text('title'); // Use text to locate widgets
find.byIcon(Icons.add); // Use Icon to locate widgets
find.byWidget(myWidget); // Locate the widget by its reference
find.byKey(Key('value')); // Use key to locate widgets
Copy the code

Test whether a widget is included in the page

The page to test is MyWidget

class MyWidget extends StatelessWidget {
  final String title;
  final String message;

  const MyWidget({Key key, @required this.title, @required this.message})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text(title), ), body: Center( child: Text(message), ), ), ); }}Copy the code

In the above page, there are two texts respectively Text (title) and Text (message). The following test class is written to verify whether the page contains two texts:

  testWidgets("MyWidget has a title and message", (WidgetTester tester) async {
    / / load MyWidget
    await tester.pumpWidget(MyWidget(
      title: "T",
      message: "M"));final titleFinder = find.text('T');
    final messageFinder = find.text('M');
    
    // Verify that the page contains the above two texts
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
Copy the code

Note: The widget to be tested needs to be wrapped with the MaterialApp();

The findsOneWidget in the code above means that a Widget corresponding to the titleFinder has been found in the page, and the findsNothing means that there is no Widget to look for in the page

Test the parts of the page that interact with the user

In the last example, we used WidgetTester to find widgets on a page. WidgetTester also helps us simulate typing, clicking, and swiping operations. Here’s another official example:

The pages to be tested are as follows:

import 'package:flutter/material.dart';

/// Date: 2019-09-29 14:44
/// Author: Liusilong
/// Description:
//

class TodoList extends StatefulWidget {
  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  static const _appTitle = 'Todo List';
  final todos = <String> [];final controller = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: Text(_appTitle),
        ),
        body: Column(
          children: <Widget>[
            TextField(
              controller: controller,
            ),
            Expanded(
              child: ListView.builder(
                  itemCount: todos.length,
                  itemBuilder: (BuildContext context, int index) {
                    final todo = todos[index];
                    return Dismissible(
                      key: Key('$todo$index'),
                      onDismissed: (direction) => todos.removeAt(index),
                      child: ListTile(title: Text(todo)),
                      background: Container(color: Colors.red),
                    );
                  }),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              if(controller.text.isNotEmpty) { todos.add(controller.text); controller.clear(); }}); }, child: Icon(Icons.add), ), ), ); }}Copy the code

The page works as follows:

The test classes are as follows:

  testWidgets('Add and remove a todo', (WidgetTester tester) async {
    // Build the widget
    await tester.pumpWidget(TodoList());
    // Enter hi in the input box
    await tester.enterText(find.byType(TextField), 'hi');
    // Click button to trigger the event
    await tester.tap(find.byType(FloatingActionButton));
    // Redraw the widget
    await tester.pump();
    // Check whether text is added to the List
    expect(find.text('hi'), findsOneWidget);

    // Test sliding
    await tester.drag(find.byType(Dismissible), Offset(500.0.0.0));

    // The page will refresh until the last frame is drawn
    await tester.pumpAndSettle();

    // Verify that the page still has the hi item
    expect(find.text('hi'), findsNothing);

  });
Copy the code

In fact, I feel that unit tests are easier to write as long as the business logic and UI are separate.

Recently, projects have been moving toward using providers for state management. I suggest a look at the Flutter Architecture – My Provider Implementation Guide series.

The general structure is as follows:

Finally, after reading the articles of My Provider Implementation Guide series, I wrote an APP. If you are interested, you can download and experience it.