Когда в приложении появляется все больше и больше функций, становится очень проблематично, когда мы хотим протестировать функцию вручную.В настоящее время нам нужно модульное тестирование, чтобы помочь нам протестировать функцию, которую мы хотим протестировать.
Во Flutter доступно три типа тестов:
- модульный тест: модульный тест
- тест виджета : тест виджета
- интеграционный тест: интеграционный тест
Первые два записаны здесь.
Когда создается новый проект Flutter, в каталоге проекта будет тестовый каталог, который используется для хранения тестовых файлов:
модульный тест
Модульные тесты используются для проверки правильности метода или части логики в коде. Шаги для написания модульных тестов следующие:
- Добавьте в проект зависимости test или flutter_test
- существуетtestСоздайте тестовый файл в каталоге, например:
counter_test.dart
- Создайте файл для тестирования, например:
counter.dart
- существует
counter_test.dart
написано в файлеtest
- Если есть несколько тестов, которые необходимо протестировать вместе, вы можете использовать
group
- запустить тестовый класс
1. Добавьте зависимости
в инженерииpubspec.yaml
добавлено вflutter_test
Зависимости:
dev_dependencies:
flutter_test:
sdk: flutter
2. Создайте тестовый файл
Здесь вам нужно создать два файла, один из которых является файлом тестового класса.counter_test.dart
Еще один тестовый файлcounter.dart
. Когда эти два файла созданы, структура каталогов выглядит следующим образом:
.
├── lib
│ ├── counter.dart
├── test
│ ├── counter_test.dart
3. Напишите тестовый класс
Counter
Методы в классе следующие:
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
4. Напишите тестовые классы
существуетcounter_test.dart
Напишите модульные тесты в файле, которые будут использовать некоторыеflutter_test
Методы верхнего уровня, предоставляемые пакетом, такие какtest(...)
Метод используется для определения теста на единицу, и естьexpect(...)
Метод используется для проверки результата.
test(...)
В методе есть два обязательных параметра: первый параметр представляет информацию описания модульного теста, а второй параметр — это функция, которая используется для записи содержимого теста.
expect(...)
В методе также есть два обязательных параметра: первый — это переменная, которую необходимо проверить, а второй — значение, соответствующее этой переменной.
counter_test.dart
Код в следующем:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/counter.dart';
/// 也可以使用命令来运行 flutter test test/counter_test.dart
void main() {
// 单一的测试
test("测试 value 递增", () {
final counter = Counter();
counter.increment();
// 验证 counter.value 的是是否为 1
expect(counter.value, 1);
});
5. Используйте группу для выполнения нескольких тестов
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/counter.dart';
void main() {
// 使用 group 合并多个测试。用来测试多个有关联的测试
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);
});
});
}
6. Выполнение модульных тестов
Если вы используете разработку Android Studio или Idea, нажмите кнопку запуска сбоку, чтобы выполнить или отладить:
Если вы используете VSCode, вы можете использовать команду для выполнения теста:
flutter test test/counter_test.dart
тест сетевого интерфейса
Точно так же вtestСоздайте новый файл в каталоге, например:http_test.dart
, в этом файле, чтобы запросить интерфейс, а затем проверить возвращенный результат:
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
void main() {
test("测试网络请求", () async {
// 假如这个请求需要一个 token
final token = "54321";
final response = await http.get(
"https://api.myjson.com/bins/18mjgh",
headers: {"token": token},
);
if (response.statusCode == 200) {
// 验证请求 header 中的 token
expect(response.request.headers['token'], token);
print(response.request.headers['token']);
print(response.body);
// 解析返回的 json
Person person = parsePersonJson(response.body);
// 验证 person 对象不为空
expect(person, isNotNull);
// 检测 person 对象中的属性值是否都正确
expect(person.name, "Lili");
expect(person.age, 20);
expect(person.country, 'China');
}
});
}
использоватьMockitoдля моделирования зависимостей объектов
Во-первых, добавьте зависимость mockito в pubspec.yaml:
dev_dependencies:
mockito: 4.1.1
Затем создайте новый класс для тестирования:
class A {
int calculate(B b) {
int randomNum = b.getRandomNum();
return randomNum * 2;
}
}
class B {
int getRandomNum() {
return Random().nextInt(100);
}
}
В приведенном выше коде метод вычисления класса A зависит от класса B. В это время тест
calculate
Метод может использовать Mockito для имитации класса B.
Затем создайте новый тестовый класс:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_testing/mock_d.dart';
import 'package:mockito/mockito.dart';
/// 使用 mockito 模拟一个类 B
class MockB extends Mock implements B {}
void main() {
test("测试使用 mockito 来 mock 依赖", () {
var b = MockB();
var a = A();
// 当调用 b.getRandomNum() 方法的时候返回 10
when(b.getRandomNum()).thenReturn(10);
expect(a.calculate(b), 20);
// 检查 b.getRandomNum(); 是否调用过
verify(b.getRandomNum());
});
}
В официальной документации есть еще один такой пример, который заключается в использовании mockito для имитации данных, возвращаемых интерфейсом.Метод, который необходимо протестировать, выглядит следующим образом:
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');
}
}
Вышеупомянутый метод заключается в запросе интерфейса.Если запрос будет успешным, он будет проанализирован и возвращен, в противном случае будет выдано исключение. Код для проверки этого метода выглядит следующим образом:
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 模拟一个 http.Client 对象
class MockClient extends Mock implements http.Client {}
void main() {
group("fetchPost", () {
test("接口返回数据正确", () async {
final client = MockClient();
// 当调用指定的接口的时候返回指定的数据
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("接口返回数据错误,抛出异常", () {
final client = MockClient();
// 当调用这个接口的时候返回 Not Found
when(client.get("https://jsonplaceholder.typicode.com/posts/1"))
.thenAnswer((_) async {
return http.Response('Not Found', 404);
});
expect(fetchPost(client), throwsException);
});
});
}
Тест виджета
Очевидная разница между тестированием виджетов и модульным тестированием заключается в том, что функция верхнего уровня, используемая при тестировании виджетов,testWidgets
, функция записывается следующим образом:
testWidgets('这是一个 Widget 测试', (WidgetTester tester){
});
мы можем использоватьWidgetTester
Чтобы создать виджет, который необходимо протестировать, или выполнить перерисовку (эквивалентно вызовуsetState(...)
метод.
Есть еще одна функция верхнего уровня.find
Чтобы найти виджет, которым нужно управлять, например:
find.text('title'); // 通过 text 来定位 widget
find.byIcon(Icons.add); // 通过 Icon 来定位 widget
find.byWidget(myWidget); // 通过 widget 的引用来定位 widget
find.byKey(Key('value')); // 通过 key 来定位 widget
Проверьте, содержит ли страница виджет
Страница 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),
),
),
);
}
}
На странице выше есть два текстовых текста (заголовок) и текст (сообщение), чтобы проверить классы тестов записи ниже, содержит ли страница два текста:
testWidgets("MyWidget has a title and message", (WidgetTester tester) async {
// 加载 MyWidget
await tester.pumpWidget(MyWidget(
title: "T",
message: "M",
));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
// 验证页面中是否含有上述的两个 Text
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
Примечание. Тестируемый виджет необходимо обернуть с помощью MaterialApp();
findsOneWidget в приведенном выше коде означает, что на странице найден виджет, соответствующий titleFinder, а соответствующий findsNothing означает, что на странице нет виджета.
Часть тестовой страницы, которая взаимодействует с пользователем
В предыдущем примере мы использовали WidgetTester для поиска виджетов на странице. WidgetTester также может помочь нам имитировать операции ввода, нажатия и перемещения. Ниже приведен официальный пример:
Тестируемая страница выглядит следующим образом:
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),
),
),
);
}
}
Страница работает следующим образом:
Тестовый класс выглядит следующим образом:
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Build the widget
await tester.pumpWidget(TodoList());
// 往输入框中输入 hi
await tester.enterText(find.byType(TextField), 'hi');
// 点击 button 来触发事件
await tester.tap(find.byType(FloatingActionButton));
// 让 widget 重绘
await tester.pump();
// 检测 text 是否添加到 List 中
expect(find.text('hi'), findsOneWidget);
// 测试滑动
await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
// 页面会一直刷新,直到最后一帧绘制完成
await tester.pumpAndSettle();
// 验证页面中是否还有 hi 这个 item
expect(find.text('hi'), findsNothing);
});
На самом деле, я считаю, что пока бизнес-логика и пользовательский интерфейс разделены, писать модульные тесты удобнее.
В последнее время проект постепенно перешел на использованиеProviderдля управления государством. Рекомендую посмотретьFlutter Architecture - My Provider Implementation GuideЭта серия статей очень хороша.
Общая структура выглядит следующим образом:
Наконец, смMy Provider Implementation GuideПосле серии статей я написалAPP, вы можете скачать и испытать его, если вам это интересно.