暴力突破 Flutter 自動化測試

1、前言


移動應用的測試每每比較複雜且工做量很大,爲了驗證用戶的真實使用體驗每每須要跨越多個平臺以及不一樣的物理設備手動測試。隨着產品功能不斷迭代累積,測試的複雜度隨之大幅增加,手動測試會變得更加困難。所以,編寫自動化測試用例對咱們的項目更新、迭代有着很是重要的做用。html

2、單元測試


單元測試是指對軟件中的最小可測試單元進行驗證的方式,使用單元測試能夠驗證單個函數、方法或類的行爲。咱們來看看 Flutter 項目的工程目錄:web

如上圖所示,lib 是 Flutter 應用源文件目錄,test 是測試文件目錄。接下來咱們看看編寫單元測試用例的步驟。json

2.1 相關步驟

2.1.1 添加依賴

Flutter 工程默認添加了 flutter_test package,若是是 dart package 沒有依賴 Flutter,能夠導入 test package,示例代碼以下:api

dev_dependencies:
  flutter_test:
    sdk: flutter
  //or
  test:
複製代碼

2.1.2 聲明一個用來測試的類

在 lib 目錄下新建一個 dart 文件,聲明一個用來測試的類,示例代碼以下:微信

//unit.dart
 
class Counter {
  int value = 0;
 
  void increment() => value++;
 
  void decrement() => value--;
}
複製代碼

2.1.3 編寫測試用例

在 test 目錄下建立一個 dart 文件(文件名建議已 _test 結尾),編寫測試用例。測試用例一般包含含定義、執行和驗證步驟,示例以下:markdown

//unit_test.dart
 
import 'package:flutter_unit_test/unit.dart';
import 'package:flutter_test/flutter_test.dart';
 
void main() {
  //第一個用例,判斷Counter對象調用increase方法後是否等於1
  test('Increase a counter value should be 1', () {
    final counter = Counter();
    counter.increase();
    expect(counter.count, 1);
  });
  
  //第二個用例,判斷1+1是否等於2
  test('1+1 should be 2', () {
    expect(1 + 1, 2);
  });
}
複製代碼

能夠看到驗證須要使用 expect 函數,將最小可測單元的執行結果與預期進行比較。 另外,測試用例須要包裝在 test() 內部,test 是 flutter 提供的測試用例封裝類。網絡

2.1.4 啓動測試用例

選擇 unit_test.dart 文件,在右鍵彈出的菜單中選擇 「Run ‘tests in widget_test’」,就能夠啓動測試用例了。運行結果以下:app

接下來咱們修改下測試用例代碼,以下:less

void main() {
  //第一個用例,判斷Counter對象調用increase方法後是否等於1
  test('Increase a counter value should be 1', () {
    final counter = Counter();
    counter.increase();
    expect(counter.count, 2);
  });
 
  //第二個用例,判斷1+1是否等於2
  test('1+1 should be 2', () {
    expect(1 + 1, 2);
  });
}
複製代碼

能夠看到,咱們將第一個用例的 1 修改爲 2 來製造一個錯誤,如今來看看測試用例執行不經過的提示:async

2.1.5 組合測試用例

若是有多個測試用例,它們之間存在關聯關係,能夠在外層使用 group 函數將它們組合在一塊兒,示例代碼以下:

void main() {
  //組合測試用例,判斷Counter對象調用increase方法後是否等於1,
  // 而且判斷Counter對象調用decrease方法後是否等
  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);
    });
  });
}
複製代碼

另外,除了上述啓動方式外,還可使用終端命令來啓動測試用例,示例以下:

//flutter test 文件路徑
flutter test test/unit_test.dart
//使用 flutter run 文件路徑 的方式來運行到真機或模擬器上測試也是能夠的
複製代碼

2.2 使用 mockito 模擬外部依賴

進行單元測試時咱們可能還須要從外部依賴(好比web服務)獲取須要測試的數據,咱們先來看一個示例,在 lib 中建立一個要測試的類:

//mock.dart
 
import 'dart:convert';
import 'package:http/http.dart' as http;
 
class Todo {
  final String title;
 
  Todo({this.title});
 
  //工廠類構造方法,將JSON轉換爲對象
  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      title: json['title'],
    );
  }
}
 
Future<Todo> fetchTodo(http.Client client) async {
  //獲取網絡數據
  final response = await client.get('https://xxx.com/todos/1');
  if (response.statusCode == 200) {
    //請求成功,解析JSON
    return Todo.fromJson(json.decode(response.body));
  } else {
    //請求失敗,拋出異常
    throw Exception('Failed to load post');
  }
}
複製代碼

能夠看到與 web 服務的數據交互是咱們程序不可以控制的,很難覆蓋全部可能成功或失敗的用例,所以更好的辦法是在測試用例中模擬這些」外部依賴「,讓其能夠返回特定內容。接下來咱們來看看使用 mockito 模擬外部依賴的步驟:

2.2.1 添加依賴

在 pubspec.yaml 文件的 dev_dependencies 中添加 mockito 包:

dependencies:
  http: ^0.12.2
 
dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito:
複製代碼

2.2.2 建立模擬類

建立一個模擬類,示例以下:

//mock_test.dart
 
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
 
class MockClient extends Mock implements http.Client {}
複製代碼

能夠看到咱們定義了一個模擬類 MockClient,這個類以接口聲明的方式獲取到了 http.Client 的外部接口。

2.2.3 編寫測試用例

如今咱們可使用 when 語句,在其調用 Web 服務時注入 MockClient 並返回相應的數據,代碼以下:

//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();
 
      //使用Mockito注入請求成功的JSON字段
      when(client.get('https://xxx.com/todos/1'))
          .thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
 
      //驗證請求結果是否爲Todo實例
      expect(await fetchTodo(client), isInstanceOf<Todo>());
    });
 
    test('throws an exception if error', () {
      final client = MockClient();
 
      //使用Mockito注入請求失敗的Error
      when(client.get('https://xxx.com/todos/1'))
          .thenAnswer((_) async => http.Response('Forbidden', 403));
 
      //驗證請求結果是否拋出異常
      expect(fetchTodo(client), throwsException);
    });
  });
}
複製代碼

能夠看到在第一個用例中咱們爲其注入了 json 結果,而在第二個用例中咱們注入了一個 403 異常。咱們來看看運行結果:

能夠看到,在沒有調用真實 Web 服務的狀況下咱們的程序成功地模擬出了正常和異常兩種狀況。

關於 Flutter 的單元測試部分先說到這裏,細心的同窗可能發現整個 Flutter 單元測試的模式跟 Android 是很是相似的。

3、UI 自動化測試


3.1 簡單示例

爲了測試 widget 類,咱們須要使用 flutter _test package。拿一個 Flutter 默認的計時器應用模板爲例:

它的 UI 測試用例能夠這麼來寫:

//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 {
    //聲明所須要驗證的Widget對象(即MyApp),並觸發其渲染
    await tester.pumpWidget(MyApp());
    //查找字符串文本爲'0'的Widget,驗證查找成功
    expect(find.text('0'), findsOneWidget);
    //查找字符串文本爲'1'的Widget,驗證查找失敗
    expect(find.text('1'), findsNothing);
    //查找'+'按鈕,施加點擊行爲
    await tester.tap(find.byIcon(Icons.add));
    //觸發其渲染
    await tester.pump();
    //查找字符串文本爲'0'的Widget,驗證查找失敗
    expect(find.text('0'), findsNothing);
    //查找字符串文本爲'1'的Widget,驗證查找成功
    expect(find.text('1'), findsOneWidget);
  });
}
複製代碼

右鍵點擊該文件,選擇 Run 'tests in widget_test.dart' 選項執行測試,測試結果以下:

3.2 相關步驟以及API詳解

flutter_test package 提供瞭如下工具用於 widget 的測試:

  • testWidgets() :此函數會自動爲每一個測試建立一個 WidgetTester,用來代替普通的 test 函數。
  • WidgetTester:使用該類可在測試環境下創建 widget 並與其交互。
  • Finder :該類能夠方便咱們在測試環境下查找 widgets。
  • Mathcer 常量:該常量在測試環境下幫助咱們驗證 Finder 是否認位到一個或多個 widgets。

接下來咱們來看看編寫測試用例的相關步驟:

3.2.1 添加 flutter_test 依賴

在 pubspec.yaml 文件的 dev_dependencies 裏添加 flutter_test 依賴,代碼以下:

dev_dependencies:
  flutter_test:
    sdk: flutter
複製代碼

3.2.2 建立用於測試的 widget

仍是拿 Flutter 默認的計時器應用模板爲例,代碼以下:

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.
    );
  }
}
複製代碼

3.2.3 建立一個 testWidgets 測試方法

用 flutter_test package 提供的 testWidgets() 函數定義一個測試。testWidgets 函數能夠定義一個 widget 測試並建立一個可使用的 WidgetTester。

import 'package:flutter_test/flutter_test.dart';
 
void main() {
  testWidgets('Counter increments UI test', (WidgetTester tester) async {
    
  });
}
複製代碼

3.2.4 使用 WidgetTester 創建並渲染 widget

在上一步中咱們建立了一個 WidgetTester,使用 WidgetTester 能夠在測試環境下創建、渲染 widget 並能夠與其進行交互。接下來咱們來介紹下 WidgetTester 中常見的 api。

建立/渲染類API

  • pumpWidget(Widget widget) :建立並渲染咱們提供的 widget。
  • pump(Duration duration):觸發 widget 重建。與 pumpWidget 不一樣之處在於即便 widget 與先前的調用相同, pumpWidget 也會強制徹底重建樹,而 pump 將僅重建已更改的 widget。例如咱們點擊調用 setState() 的按鈕,可使用 pump 方法來讓 flutter 再一次創建咱們的 widget。
  • pumpAndSettle():在給按期間內不斷重複調用 pump() 直到完成全部繪製幀,通常須要等到全部動畫所有完成。

交互類API

  • enterText():模擬輸入文本。
  • tap():模擬點擊按鈕。
  • drag():模擬滑動。
  • longPress():模擬長按。

其餘方法這裏再也不贅述,若是想深刻理解這些內容,能夠參考 WidgetTester 進行學習。

3.2.5 使用 Finder 定位(查找) widget

在測試環境下,爲了定位 widget,咱們須要用到 Finder 類。

  • text(String text):查找含有特定文本的 widget,例如 find.text('0')。
  • widgetWithText():限定 widget 的類型,而且該類型 widget 包含給定的文本,例如 find.widgetWithText(Button, '0')。
  • byKey(Key key):使用具體 key 查找 widget。例如 find.byKey(Key('H'))。
  • byType(Type type):根據 type 來尋找對應的 widget,type 參數必須是 Widget 的子類,例如 find.byType(IconButton)。
  • byWidget(Widget widget):根據 widget 實例來尋找對應的 widget。示例以下:
Widget myButton = new Button(
  child: new Text('Update')
);
 
find.byWidget(myButton);
複製代碼
  • byWidgetPredicate():根據 widget 的屬性匹配 widget,示例以下:
find.byWidgetPredicate(
  (Widget widget) => widget is Tooltip && widget.message == 'Back',
  description: 'widget with tooltip "Back"',
)
複製代碼

若是想深刻理解這些內容,能夠參考 CommonFinders 進行學習。

3.2.六、使用 Matcher 常量進行驗證

flutter_test 提供瞭如下 matchers:

  • findsOneWidget:找到一個 widget
  • findsWidgets:找到一個或多個 widget
  • findsNothing:沒有找到 widget
  • findsNWidgets:找到指定數量的 widget

例如:

//查找字符串文本爲'0'的Widget,驗證查找失敗
expect(find.text('0'), findsNothing);
複製代碼

經過以上步驟,咱們對 widget 測試有了必定的瞭解了,如今咱們再來看看上面寫的那個 widget 測試用例能夠有更深入的認識了:

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 {
    //聲明所須要驗證的Widget對象(即MyApp),並觸發其渲染
    await tester.pumpWidget(MyApp());
    //查找字符串文本爲'0'的Widget,驗證查找成功
    expect(find.text('0'), findsOneWidget);
    //查找字符串文本爲'1'的Widget,驗證查找失敗
    expect(find.text('1'), findsNothing);
    //查找'+'按鈕,施加點擊行爲
    await tester.tap(find.byIcon(Icons.add));
    //觸發其渲染
    await tester.pump();
    //查找字符串文本爲'0'的Widget,驗證查找失敗
    expect(find.text('0'), findsNothing);
    //查找字符串文本爲'1'的Widget,驗證查找成功
    expect(find.text('1'), findsOneWidget);
  });
}
複製代碼

儘管 widget 測試擴大了應用的測試範圍,能夠找到單元測試沒法找到的問題,不過相比於單元測試來講,widget 測試用例的開發和維護成本很是高,所以建議在項目達到必定的規模,而且業務特徵具備必定的延續規律後,再考慮 widget 測試的必要性。

更多精彩內容,盡請關注騰訊VTeam技術團隊微信公衆號和視頻號

原做者:但超

未經贊成,禁止轉載!

相關文章
相關標籤/搜索