Testing on Flutter
06 Jul 2020 |Testing
Flutter에서 testing은 3가지로 나눌 수 있다.
- Unit Test 하나의 function, method, class를 test. 정해진 condition에서 logic이 제대로 돌아가는 확인하는 것이 목표.
- Widget Test 하나의 widget을 test. Widget이 원하는 대로 작동하는지 UI도 원하는 대로 나타나는지 확인하는 것이 목표.
- Integration Test 전체 앱이나 앱의 큰 부분을 test. 전체 widget과 service가 합쳐져서 잘 돌아가는지 확인하는 것이 목표. 또한 전체 앱의 퍼포먼스도 확인해 볼 수 있다.
Unit이 모여 Widget이 되고 Widget이 모여 Integration이 된다.
Unit testing example
Counter
counter.dart파일을 lib 폴더에, counter_test.dart파일을 test폴더에 넣고 counter_test.dart를 run시킴.
# add test dependency
dev_dependencies:
test:
// counter.dart
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
// counter_test.dart
import 'package:test/test.dart';
import 'package:your_project_name/counter.dart';
void main() {
group('Counter', () {
// expect에서 앞에 해당하는 것과 뒤에 해당하는 것이 일치할 경우 success, 아니면 fail
test('value should start at 0', () {
expect(Counter().value, 0);
});
test('value should be incremented', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
test('value should be decremented', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
});
}
expect가 제대로 안되었을 경우 Expected값과 Actual값을 보여준다.
Mock
unit test의 경우 web service나 DB에서 데이터 가져올 경우 test가 느려지기도 하고 데이터 가져오는게 실패할 수도 있는 등 여러 변수가 있는데 이를 mock를 사용해서 해결. Mock는 service나 DB를 모방해서 상황에 따라 특정 결과 값을 반환하도록 함. Mock은 훌륭한 대체재!
(근데 이 예제 안돌아감…)
# add package dependencies
dependencies:
http:
dev_dependencies:
flutter_test:
sdk: flutter
mockito:
// main
class Post {
dynamic data;
Post.fromJson(this.data);
}
Future<Post> fetchPost(http.Client client) async {
final response =
await client.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON.
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
// test
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
// Create a MockClient using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockClient extends Mock implements http.Client {}
main() {
group('fetchPost', () {
test('returns a Post if the http call completes successfully', () async {
final client = MockClient();
// Use Mockito to return a successful response when it calls the
// provided http.Client.
when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
.thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
expect(await fetchPost(client), isA<Post>());
});
test('throws an exception if the http call completes with an error', () {
final client = MockClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
.thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchPost(client), throwsException);
});
});
}
위 예제를 cookbook에서는 dart test/fetch_post_test.dart를 command에서 돌리라고 하는데 이러면 안돌아간다… ㅠ 근데 run 시키면 제대로 돌아가는 것은 확인
Widget testing example
# add package dependency
dev_dependencies:
flutter_test:
sdk: flutter
// widget test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// Define a test. The TestWidgets function also provides a WidgetTester
// to work with. The WidgetTester allows building and interacting
// with widgets in the test environment.
testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
// Create the widget by telling the tester to build it.
// widget을 pumb시켜 가볍게 하나 만든다는 느낌.
await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));
// Create the Finders.
final titleFinder = find.text('T');
final messageFinder = find.text('M');
// Use the `findsOneWidget` matcher provided by flutter_test to
// verify that the Text widgets appear exactly once in the widget tree.
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
/*
findsOneWidget: Verifies that one widget is found.
findsNothing: Verifies that no widgets are found.
findsWidgets: Verifies that one or more widgets are found.
findsNWidgets: Verifies that a specific number of widgets are found.
*/
// 다음과 같이하면 titleFinder에 해당하는 위젯이 1개 있는데 nothing이라고 했으니 맞지 않으므로 fail
expect(titleFinder, findsNothing);
});
}
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),
),
),
);
}
}
// another test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('finds a Text widget', (WidgetTester tester) async {
// Build an App with a Text widget that displays the letter 'H'.
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Text('H'),
),
));
// Find a widget that displays the letter 'H'.
expect(find.text('H'), findsOneWidget);
});
testWidgets('finds a widget using a Key', (WidgetTester tester) async {
// Define the test key.
final testKey = Key('K');
// Build a MaterialApp with the testKey.
await tester.pumpWidget(MaterialApp(key: testKey, home: Container()));
// Find the MaterialApp widget using the testKey.
expect(find.byKey(testKey), findsOneWidget);
});
testWidgets('finds a specific instance', (WidgetTester tester) async {
final childWidget = Padding(padding: EdgeInsets.zero);
// Provide the childWidget to the Container.
await tester.pumpWidget(Container(child: childWidget));
// Search for the childWidget in the tree and verify it exists.
expect(find.byWidget(childWidget), findsOneWidget);
});
}
Integration testing example
Counter
# Add the flutter_driver dependency
dev_dependencies:
flutter_driver:
sdk: flutter
test: any
다음과 같이 파일을 구성
counter_app/
lib/
main.dart
test_driver/
app.dart
app_test.dart
main.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App',
home: MyHomePage(title: 'Counter App 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',
// Provide a Key to this specific Text widget. This allows
// identifing the widget from inside the test suite,
// and reading the text.
key: Key('counter'),
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
// Provide a Key to this button. This allows finding this
// specific button inside the test suite, and tapping it.
key: Key('increment'),
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:counter_app/main.dart' as app;
void main() {
// This line enables the extension.
enableFlutterDriverExtension();
// Call the `main()` function of the app, or call `runApp` with
// any widget you are interested in testing.
app.main();
}
app_test.dart
// Imports the Flutter Driver API.
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Counter App', () {
// First, define the Finders and use them to locate widgets from the
// test suite. Note: the Strings provided to the `byValueKey` method must
// be the same as the Strings we used for the Keys in step 1.
final counterTextFinder = find.byValueKey('counter');
final buttonFinder = find.byValueKey('increment');
FlutterDriver driver;
// Connect to the Flutter driver before running any tests.
setUpAll(() async {
driver = await FlutterDriver.connect();
});
// Close the connection to the driver after the tests have completed.
tearDownAll(() async {
if (driver != null) {
driver.close();
}
});
test('starts at 0', () async {
// Use the `driver.getText` method to verify the counter starts at 0.
expect(await driver.getText(counterTextFinder), "0");
});
test('increments the counter', () async {
// First, tap the button.
await driver.tap(buttonFinder);
// Then, verify the counter text is incremented by 1.
expect(await driver.getText(counterTextFinder), "1");
});
});
}