ํ ์คํธ - ๋จ์, ์์ ฏ, ํตํฉ
์ง๊ธ๊น์ง๋ ์ฝ๋๋ฅผ ๊ณ ์น ๋๋ง๋ค ์ฑ์ ์ง์ ์คํํด์ ๋๋ฌ๋ณด๋ฉฐ ํ์ธํ๋ค. ํ๋ฉด์ด ์ด ๊ฐ๋ฅผ ๋๊ณ ๊ธฐ๋ฅ์ด ๋๋ฉด ์ด ๋ฐฉ์์ ํ๊ณ๊ฐ ์จ๋ค. ํ ๊ณณ์ ๊ณ ์น๊ณ ๋ค๋ฅธ ๊ณณ์ด ๊นจ์ก๋์ง ๋งค๋ฒ ๋ค ๋๋ฌ๋ณผ ์๋ ์๊ธฐ ๋๋ฌธ์ด๋ค. ํ ์คํธ๋ ์ด ํ์ธ ์์ ์ ์ฝ๋๋ก ์ ์ด๋๊ณ ๋ช ๋ น ํ๋๋ก ๋ค์ ๋๋ฆฌ๋ ๊ฒ์ด๋ค.
Flutter์๋ ์ธ ์ข ๋ฅ์ ํ ์คํธ๊ฐ ์๋ค. ๋ฒ์๊ฐ ์ข๊ณ ๋น ๋ฅธ ์์๋ก ์ด๋ ๋ค.
- ๋จ์ ํ ์คํธ(unit): ํจ์๋ ํด๋์ค ํ๋๊ฐ ๋ง๊ฒ ๋์ํ๋์ง ๋ณธ๋ค. ํ๋ฉด ์์ด Dart ์ฝ๋๋ง ๋๋ ค์ ๋งค์ฐ ๋น ๋ฅด๋ค
- ์์ ฏ ํ
์คํธ(widget): ์์ ฏ ํ๋๋ฅผ ๊ฐ์์ ํ๋ฉด์ ์ฌ๋ ค์ ๋ณด์ด๋ ๊ฒ๊ณผ ๋๋ฅด๊ธฐ ๋์์ ํ์ธํ๋ค. ๊ธฐ๊ธฐ ์์ด
flutter test๋ก ๋๋ค - ํตํฉ ํ ์คํธ(integration): ์ง์ง ์ฑ์ ๊ธฐ๊ธฐ๋ ์๋ฎฌ๋ ์ดํฐ์์ ๋๋ ค์ ์ฌ์ฉ์์ฒ๋ผ ์ฒ์๋ถํฐ ๋๊น์ง ๋๋ฌ๋ณธ๋ค. ๊ฐ์ฅ ๋๋ฆฌ๋ค
๋น ๋ฅด๊ณ ๋ง์ด โ ๋๋ฆฌ๊ณ ์ ๊ฒ๊ฐ ๊ธฐ๋ณธ ๋น์จ์ด๋ค. ๋จ์ ํ ์คํธ๋ฅผ ๊ฐ์ฅ ๋ง์ด, ํตํฉ ํ ์คํธ๋ ํต์ฌ ํ๋ฆ ๋ช ๊ฐ๋ง ์ด๋ค.
1๋จ๊ณ: ๋จ์ ํ ์คํธ - ๊ฐ์ฅ ๋จผ์ , ๊ฐ์ฅ ๋ง์ด
๊ฐ๋จํ ํด๋์ค๋ถํฐ ํด๋ณด์.
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}ํ
์คํธ ํ์ผ์ ํ๋ก์ ํธ์ test/ ํด๋์ ๋๊ณ , ํ์ผ ์ด๋ฆ์ _test.dart๋ก ๋๋ธ๋ค. test/counter_test.dart๋ฅผ ๋ง๋ ๋ค.
import 'package:flutter_test/flutter_test.dart';
void main() {
test('increment๋ ๊ฐ์ 1 ์ฌ๋ฆฐ๋ค', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
test('decrement๋ ๊ฐ์ 1 ๋ด๋ฆฐ๋ค', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
}flutter testtest('์ค๋ช ', () { ... }): ํ ์คํธ ํ๋๋ค. ์ค๋ช ์ ์คํจํ์ ๋ ์ด๋ ํ ์คํธ์ธ์ง ์๋ ค์ฃผ๋ ์ด๋ฆ์ด๋คexpect(์ค์ ๊ฐ, ๊ธฐ๋๊ฐ): ์ค์ ๊ฐ์ด ๊ธฐ๋ํ ๊ฒ๊ณผ ๊ฐ์์ง ํ์ธํ๋ค. ๋ค๋ฅด๋ฉด ๊ทธ ํ ์คํธ๊ฐ ์คํจํ๋ค- ์ค๋น(Arrange), ์คํ(Act), ํ์ธ(Assert) ์ธ ๋จ๊ณ๋ก ์ฐ๋ฉด ์ฝ๊ธฐ ์ฝ๋ค. ์ ์ฝ๋์ ๋น ์ค์ด ๊ทธ ๊ตฌ๋ถ์ด๋ค
ํ
์คํธ๊ฐ ์ฌ๋ฌ ๊ฐ๋ฉด group์ผ๋ก ๋ฌถ๊ณ , ๋งค๋ฒ ๋ฐ๋ณตํ๋ ์ค๋น๋ setUp์ ๋๋ค.
void main() {
group('Counter', () {
late Counter counter;
setUp(() {
counter = Counter(); // ํ
์คํธ๋ง๋ค ์๋ก ๋ง๋ค์ด์ง
});
test('์ฒ์ ๊ฐ์ 0์ด๋ค', () => expect(counter.value, 0));
test('increment๋ 1 ์ฌ๋ฆฐ๋ค', () {
counter.increment();
expect(counter.value, 1);
});
});
}late์ setUp์ ์กฐํฉ์ ์ฒซ ๊ธ์ late final + initState์ ๊ฐ์ ๋ชจ์์ด๋ค. setUp์ด ๊ฐ ํ
์คํธ๋ณด๋ค ๋จผ์ ์คํ๋๋ค๋ ์์๋ฅผ ๋ฏฟ๊ณ ์ฐ๋ ๊ฒ์ด๋ค.
2๋จ๊ณ: ๊ฐ์ง๋ก ๋ฐ๊ฟ ๋ผ์ฐ๋ ํ ์คํธ - mocktail
์ค์ ์ฑ์์ ์ํํ๊ณ ์ถ์ ๊ฑด UserRepository ๊ฐ์ ํด๋์ค๋ค. ์ด๊ฑด ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ UserApiService์ ์์กดํ๋ค. ์๋ฒ์ ์ง์ง๋ก ์์ฒญํ๋ฉด ํ
์คํธ๊ฐ ๋๋ฆฌ๊ณ , ์ธํฐ๋ท์ด ์์ผ๋ฉด ์คํจํ๋ค. Repository ๊ธ๊ณผ ์์กด์ฑ ์ฃผ์
๊ธ์์ ๊ฐ์ง๋ฅผ ๋ผ์ฐ๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด ๋ ์ด์ ๊ฐ ์ฌ๊ธฐ์ ๋์จ๋ค. ๊ฐ์ง๋ฅผ ์ฝ๊ฒ ๋ง๋ค์ด์ฃผ๋ mocktail์ ์ค์นํ๋ค.
flutter pub add dev:mocktailimport 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockUserApiService extends Mock implements UserApiService {}
void main() {
late MockUserApiService service;
late UserRepositoryImpl repo;
setUp(() {
service = MockUserApiService();
repo = UserRepositoryImpl(service); // ์์ฑ์๋ก ๊ฐ์ง๋ฅผ ๋ฃ๋๋ค
});
test('getUsers๋ JSON์ User ๋ชฉ๋ก์ผ๋ก ๋ฐ๊พผ๋ค', () async {
when(() => service.fetchUsers()).thenAnswer(
(_) async => [
{'id': 1, 'name': 'ํ๊ธธ๋'},
],
);
final users = await repo.getUsers();
expect(users.single.name, 'ํ๊ธธ๋');
verify(() => service.fetchUsers()).called(1);
});
test('์๋ฒ๊ฐ ์คํจํ๋ฉด ์๋ฌ๊ฐ ๊ทธ๋๋ก ์ฌ๋ผ์จ๋ค', () {
when(() => service.fetchUsers()).thenThrow(Exception('์๋ฒ ์ค๋ฅ'));
expect(() => repo.getUsers(), throwsA(isA<Exception>()));
});
}class MockUserApiService extends Mock implements UserApiService {}: ์์ ๊ธ์implements๋ก,UserApiService์ ๊ฐ์ ๋ชจ์์ ๊ฐ์ง๋ฅผ ๋ง๋ ๋ค.Mock์ด ํธ์ถ์ ๊ธฐ๋กํ๊ณ ์ ํด๋ ๋ต์ ๋๋ ค์ค๋คwhen(() => service.fetchUsers()).thenAnswer(...): "์ด ํจ์๊ฐ ๋ถ๋ฆฌ๋ฉด ์ด๋ ๊ฒ ๋ตํด๋ผ"๋ฅผ ์ ํ๋ค.Future๋ฅผ ๋๋ ค์ฃผ๋ ํจ์๋thenAnswer๋ฅผ ์ด๋คthenThrow(...): ์ผ๋ถ๋ฌ ์คํจํ๊ฒ ๋ง๋ ๋ค. ์๋ฒ๊ฐ ์คํจํ์ ๋๋ฅผ ์๋ฒ ์์ด ์ํํ ์ ์๋คverify(...).called(1): ๊ทธ ํจ์๊ฐ ์ ํํ ํ ๋ฒ ๋ถ๋ ธ๋์ง ํ์ธํ๋คthrowsA(isA<Exception>()): ์๋ฌ๊ฐ ๋์ ธ์ง๋์ง ํ์ธํ๋ค
์๋ฒ ์์ด ์๋ฌ ์ํฉ๊น์ง ์ํํ ์ ์๋ค๋ ๊ฒ ๊ฐ์ฅ ํฐ ํจ๊ณผ๋ค. ์๋ฌ ์ฒ๋ฆฌ ๊ธ์์ ๋ง๋ try/catch์ ์๋ฌ ํ๋ฉด์ด ์ ๋๋ก ๋์ํ๋์ง๋ ์ด๋ ๊ฒ ํ์ธํ๋ค.
์ฝ์ง ํฌ์ธํธ: mocktail์์ ์์ฃผ ๋งํ๋ ๊ณณ
- ์ ํด๋์ง ์์ ํจ์๋ฅผ ๋ถ๋ฅด๋ฉด ์๋ฌ๊ฐ ๋๋ค.
when์ผ๋ก ๋ต์ ์ ํ์ง ์์ ํจ์๋ฅผ ์ฝ๋๊ฐ ๋ถ๋ฅด๋ฉดnull์ด ๋์์์ ํ์ ์๋ฌ๊ฐ ๋๋ค. ์ํํ๋ ์ฝ๋๊ฐ ํธ์ถํ๋ ํจ์๋ ๋ชจ๋when์ผ๋ก ์ ํด๋๋ค any()๋ฅผ ๋ด๊ฐ ๋ง๋ ํด๋์ค์ ์ฐ๋ฉด ๋งํ๋ค.when(() => repo.save(any()))์ฒ๋ผUser๋ฅผany()๋ก ๋ฐ์ผ๋ ค๋ฉด, ๋จผ์ registerFallbackValue(User(...))๋ก ์์ ๊ฐ์ ํ๋ ๋ฑ๋กํด์ผ ํ๋ค.int,String๊ฐ์ ๊ธฐ๋ณธ ํ์ ์ ํ์ ์๋คMock์implements๋ก ๋ง๋ค์์ผ๋ ์ถ์ ํด๋์ค๋ฅผ ๋ฐ๋ก ๊ฐ์ง๋ก ๋ง๋ค ์ ์๋ค. Repository๋ฅผabstract class๋ก ์ ์ํด ๋์๋ค๋ฉดMockUserRepository๋ฅผ ๊ทธ๋ฅ ๋ง๋ค๋ฉด ๋๋ค
3๋จ๊ณ: ์์ ฏ ํ ์คํธ - ํ๋ฉด์ด ๋ณด์ด๊ณ ๋๋ฌ์ง๋์ง
๋ฒํผ์ ๋๋ฅด๋ฉด ์ซ์๊ฐ ์ฌ๋ผ๊ฐ๋ ์์ ฏ์ด ์๋ค๊ณ ํ์.
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('$count'),
ElevatedButton(
key: const Key('increment'),
onPressed: () => setState(() => count++),
child: const Text('+1'),
),
],
),
);
}
}import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('๋ฒํผ์ ๋๋ฅด๋ฉด ์ซ์๊ฐ 1 ์ฌ๋ผ๊ฐ๋ค', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byKey(const Key('increment')));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
}testWidgets: ์์ ฏ ํ ์คํธ๋ค.tester๊ฐ ๊ฐ์์ ํ๋ฉด์ ์กฐ์ข ํ๋ ๋๊ตฌ๋คtester.pumpWidget(...): ์์ ฏ์ ๊ฐ์ ํ๋ฉด์ ์ฌ๋ฆฐ๋คfind.text('0'),find.byKey(...),find.byType(...): ํ๋ฉด์์ ์์ ฏ์ ์ฐพ๋ ํ์ธ๋๋คfindsOneWidget,findsNothing: ์ฐพ์ ๊ฐ์๊ฐ ํ๋์ธ์ง, ํ๋๋ ์๋์ง ํ์ธํ๋คtester.tap(...): ๋๋ฅธ๋ค. ๊ธ์๋ฅผ ์ ๋ ฅํ๋ ค๋ฉดtester.enterText(...)๋ฅผ ์ด๋คtester.pump(): ํ๋ฉด์ ํ ํ๋ ์ ๋ค์ ๊ทธ๋ฆฐ๋ค.setStateํ์ ์ด๊ฑธ ๋ถ๋ฌ์ผ ๋ฐ๋ ํ๋ฉด์ด ๋ฐ์๋๋ค
ํ์ธ๋๋ find.text๋ณด๋ค find.byKey๋ find.byType์ด ์ค๋๊ฐ๋ค. ๊ธ์๋ ์ธ์ด๋ฅผ ๋ฐ๊พธ๊ฑฐ๋ ๋ฌธ๊ตฌ๋ฅผ ๊ณ ์น๋ฉด ํ
์คํธ๊ฐ ๊นจ์ง์ง๋ง, ํค๋ ๊ทธ๋๋ก์ด๊ธฐ ๋๋ฌธ์ด๋ค.
์ฝ์ง ํฌ์ธํธ: ์์ ฏ ํ ์คํธ์์ ์์ฃผ ๋งํ๋ ๊ณณ
MaterialApp์ผ๋ก ๊ฐ์ธ์ง ์์ผ๋ฉด ์๋ฌ๊ฐ ๋๋ค. ์์ ฏ ๊ธ์์Text๋ ์์์ ๋ด๋ ค์ค๋ ๋ฐฉํฅ๊ณผ ํ ๋ง๊ฐ ํ์ํ๋ค๊ณ ํ๋ค. ํ ์คํธ์์๋ ๋ง์ฐฌ๊ฐ์ง๋ผ์Directionality๋Material์ด ์๋ค๋ ์๋ฌ๊ฐ ๋๋ค.MaterialApp(home: ...)์ผ๋ก ๊ฐ์ธ์ ์ฌ๋ฆฐ๋คpump์pumpAndSettle์ ๊ตฌ๋ถํ๋ค.pump()๋ ํ ํ๋ ์๋ง ๊ทธ๋ฆฐ๋ค.pumpAndSettle()์ ์ ๋๋ฉ์ด์ ์ด ๋๋ ๋๊น์ง ๋ฐ๋ณตํด์ ๊ทธ๋ฆฐ๋ค. ๊ทธ๋ฐ๋ฐ ๋๋์ง ์๋ ์ ๋๋ฉ์ด์ (๊ณ์ ๋๋CircularProgressIndicator๋ฑ)์ด ํ๋ฉด์ ์์ผ๋ฉดpumpAndSettle์ด ๋๋์ง ์์ ์๊ฐ ์ด๊ณผ ์๋ฌ๊ฐ ๋๋ค. ๊ทธ๋ด ๋๋pump(const Duration(milliseconds: 100))๋ก ์ง์ ์๊ฐ์ ์งํ์ํจ๋ค- ์ฐพ๋ ๊ธ์๊ฐ ํ๋ฉด์ ์๋ค.
tap์ด๋expect์ ์pump๋ฅผ ์ ๋ถ๋ฅด๋ฉด ์์ง ๊ทธ๋ ค์ง์ง ์์ ์ํ๋ค
์ํ ๊ด๋ฆฌ์ ํจ๊ป ์ํํ๊ธฐ
Riverpod์ผ๋ก ๋ง๋ ํ๋ฉด์ ProviderScope์ overrides๋ก ๊ฐ์ง Repository๋ฅผ ๋ผ์์ ์ํํ๋ค. ์์กด์ฑ ์ฃผ์
๊ธ์์ ๋ณธ ๊ทธ ๊ตฌ์กฐ๋ค.
testWidgets('์ฌ์ฉ์ ๋ชฉ๋ก์ด ํ๋ฉด์ ๋ณด์ธ๋ค', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userRepositoryProvider.overrideWithValue(FakeUserRepository()),
],
child: const MaterialApp(home: UserListView()),
),
);
await tester.pumpAndSettle(); // ๋ฐ์ดํฐ๊ฐ ๋์ฐฉํ ๋๊น์ง
expect(find.text('ํ๊ธธ๋'), findsOneWidget);
});์๋ฒ ์์ด "๋ก๋ฉ์ด ๋๋๋ฉด ์ด๋ฆ์ด ๋ณด์ธ๋ค"๋ฅผ ์ํํ๋ค. ์๋ฌ ์ํ๋ thenThrow๋ฅผ ์ฐ๋ ๊ฐ์ง๋ก ๋ฐ๊ฟ์ "์๋ฌ ๋ฌธ๊ตฌ์ ๋ค์ ์๋ ๋ฒํผ์ด ๋ณด์ธ๋ค"๋ฅผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ํ์ธํ๋ค. Bloc์ bloc_test ํจํค์ง๊ฐ Cubit์ ์ํ ๋ณํ๋ฅผ ์์๋๋ก ํ์ธํ๋ ๋๊ตฌ๋ฅผ ์ค๋ค.
4๋จ๊ณ: ํตํฉ ํ ์คํธ - ์ง์ง ์ฑ์ ์ฒ์๋ถํฐ ๋๊น์ง
๋จ์, ์์ ฏ ํ ์คํธ๋ ์ผ๋ถ๋ถ๋ง ๋ณธ๋ค. ๋ก๊ทธ์ธ ํ ๋ชฉ๋ก์ ๋ถ๋ฌ์์ ์์ธ ํ๋ฉด๊น์ง ๊ฐ๋ ์ ์ฒด ํ๋ฆ์ด ์ด์ด์ง๋์ง๋ ํตํฉ ํ ์คํธ๋ก ๋ณธ๋ค. ์ค์น๋ถํฐ ํ๋ค.
flutter pub add 'dev:integration_test:{"sdk":"flutter"}'integration_test/ ํด๋๋ฅผ ๋ง๋ค๊ณ app_test.dart๋ฅผ ์ด๋ค.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('๋ก๊ทธ์ธ ํ ํ์ ๋ฌธ๊ตฌ๊ฐ ๋ณด์ธ๋ค', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email')), 'test@mail.com');
await tester.enterText(find.byKey(const Key('password')), '123456');
await tester.tap(find.byKey(const Key('login')));
await tester.pumpAndSettle();
expect(find.text('ํ์ํฉ๋๋ค'), findsOneWidget);
});
}flutter test integration_test/app_test.dart์์ ฏ ํ
์คํธ์ ์ฐ๋ ๋๊ตฌ(tester, find, expect)๊ฐ ๊ฐ๋ค. ๋ฌ๋ผ์ง๋ ๊ฑด ์ง์ง ์ฑ์ด ๊ธฐ๊ธฐ๋ ์๋ฎฌ๋ ์ดํฐ์์ ์คํ๋๋ค๋ ์ ์ด๋ค. ๊ทธ๋์ ๋๋ฆฌ๊ณ , ์๋ฒ๋ ๋ก๊ทธ์ธ ๊ฐ์ ํ๊ฒฝ์ด ์ค๋น๋์ด ์์ด์ผ ํ๋ค. ๊ฒฐ์ ๋ ๋ก๊ทธ์ธ์ฒ๋ผ ๊นจ์ง๋ฉด ์ ๋๋ ํต์ฌ ํ๋ฆ ๋ช ๊ฐ๋ง ํตํฉ ํ
์คํธ๋ก ๋๊ณ , ๋๋จธ์ง๋ ๋จ์์ ์์ ฏ ํ
์คํธ๋ก ์ฑ์ด๋ค.
๋ฌด์์ ์ํํ๋
Flutter ๊ณต์ ์ํคํ ์ฒ ๊ฐ์ด๋๋ Service, Repository, ViewModel ํด๋์ค๋ง๋ค ๋จ์ ํ ์คํธ๋ฅผ ์ฐ๋ผ๊ณ ๊ถํ๋ค. ํ๋ฉด์ ๊ณ์ธต์ผ๋ก ๋๋ ๋ ๋๋ถ์ ๊ฐ๋ฅํ ์ผ์ด๋ค. ์์ํ ๋๋ ์ด ์์๊ฐ ๋ถ๋ด์ด ์ ๋ค.
- 1์์: ๊ณ์ฐ, ๋ณํ, ๊ฒ์ฆ ๊ฐ์ ์์ํ ๋ก์ง (๋จ์ ํ ์คํธ)
- 2์์: Repository์ ViewModel (๊ฐ์ง๋ฅผ ๋ผ์ด ๋จ์ ํ ์คํธ)
- 3์์: ์๋ฌ ํ๋ฉด, ๋น ๋ชฉ๋ก ๊ฐ์ ํ๋ฉด ์ํ (์์ ฏ ํ ์คํธ)
- 4์์: ๋ก๊ทธ์ธ์์ ๊ฒฐ์ ๊น์ง ๊ฐ์ ํต์ฌ ํ๋ฆ (ํตํฉ ํ ์คํธ)
Android, iOS์ ๋น๊ตํ๋ฉด
// Kotlin - JUnit + MockK, Compose ํ
์คํธ
@Test fun increment() { val c = Counter(); c.increment(); assertEquals(1, c.value) }
@get:Rule val rule = createComposeRule()
@Test fun click() {
rule.setContent { CounterScreen() }
rule.onNodeWithTag("increment").performClick()
rule.onNodeWithText("1").assertExists()
}// Swift - XCTest
func testIncrement() {
let counter = Counter()
counter.increment()
XCTAssertEqual(counter.value, 1)
}- ๋จ์ ํ
์คํธ: Flutter
test+expect/ Android JUnit +assertEquals/ iOS XCTest +XCTAssertEqual - ๊ฐ์ง ๋ง๋ค๊ธฐ: Flutter
mocktail/ Android MockK, Mockito / iOS protocol์ ๋ฐ๋ฅด๋ ๊ฐ์ง ์ง์ ์์ฑ - ํ๋ฉด ํ
์คํธ: Flutter
testWidgets,find,tester.tap/ ComposecreateComposeRule,onNodeWithText,performClick/ iOS๋ XCUITest๋ ViewInspector - ํตํฉ ํ
์คํธ: Flutter
integration_test/ Android Espresso, Compose UI ํ ์คํธ / iOS XCUITest - ๋ช
๋ น: Flutter
flutter test/ Android./gradlew test/ iOS Xcode์ Test
Compose์ createComposeRule๊ณผ Flutter์ testWidgets๊ฐ ๊ฑฐ์ ๊ฐ์ ์ญํ ์ด๋ค. ๋ ๋ค ๊ธฐ๊ธฐ ์์ด ํ๋ฉด์ ์ฌ๋ ค์ ์ฐพ๊ณ ๋๋ฅด๊ณ ํ์ธํ๋ค. Flutter๋ flutter test ํ ์ค๋ก ๋จ์์ ์์ ฏ ํ
์คํธ๋ฅผ ๊ฐ์ด ๋๋ฆฌ๊ณ , ํตํฉ ํ
์คํธ๋ง ๋ณ๋ ๋ช
๋ น์ด๋ผ๋ ์ ์ด ๊ฐ๋จํ๋ค.
์ ๋ฆฌ
- ํ
์คํธ๋ ํ์ธ ์์
์ ์ฝ๋๋ก ์ ์ด๋๊ณ
flutter test๋ก ๋ค์ ๋๋ฆฌ๋ ๊ฒ์ด๊ณ , ๋จ์(๋น ๋ฅด๊ณ ๋ง์ด), ์์ ฏ(ํ๋ฉด ํ ์กฐ๊ฐ), ํตํฉ(์ง์ง ์ฑ, ๋๋ฆฌ๊ณ ์ ๊ฒ) ์ธ ์ข ๋ฅ๊ฐ ์๋ค - ๋จ์ ํ
์คํธ๋
test/ํด๋์_test.dartํ์ผ์test์expect๋ก ์ฐ๊ณ , ๋ฐ๋ณต๋๋ ์ค๋น๋setUp์ ๋๋ค - ์๋ฒ ๊ฐ์ ์์กด์ฑ์
mocktail์Mock์ผ๋ก ๊ฐ์ง๋ฅผ ๋ง๋ค์ดwhen(...).thenAnswer๋ก ๋ต์ ์ ํ๊ณ ,thenThrow๋ก ์๋ฌ ์ํฉ๋ ์ํํ๋ค - ์์ ฏ ํ
์คํธ๋
testWidgets์์pumpWidget์ผ๋ก ์ฌ๋ฆฌ๊ณ ,find๋ก ์ฐพ๊ณ ,tester.tap๋ค์pump๋ฅผ ๋ถ๋ฌ ํ์ธํ๋ค.MaterialApp์ผ๋ก ๊ฐ์ธ์ผ ํ๊ณ , ๋๋์ง ์๋ ์ ๋๋ฉ์ด์ ์ด ์์ผ๋ฉดpumpAndSettle์ด ์๊ฐ ์ด๊ณผ๋๋ค - Riverpod ํ๋ฉด์
overrides๋ก ๊ฐ์ง Repository๋ฅผ ๋ผ์์ ์๋ฒ ์์ด ์ํํ๊ณ , ํตํฉ ํ ์คํธ๋ ํต์ฌ ํ๋ฆ ๋ช ๊ฐ๋งintegration_test๋ก ์ด๋ค - ๊ณ์ธต์ ๋๋ ๋ ๋๋ถ์ Service, Repository, ViewModel๋ง๋ค ๋จ์ ํ ์คํธ๋ฅผ ์ธ ์ ์๋ค
๋ค์ ๊ธ์์๋ ์ฑ์ ์ฌ๋ฌ ์ธ์ด๋ก ๋ณด์ฌ์ฃผ๋ ๋ค๊ตญ์ด(l10n)์ ์ฑ ์์ด์ฝ, ์คํ๋์ ํ๋ฉด ์ค์ ์ ๋ค๋ค๋ณด๊ฒ ๋ค.
