Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Flutter] Unitテストの基礎を理解する

h.uriu
June 25, 2024

[Flutter] Unitテストの基礎を理解する

「モバイルアプリ開発における良いテストコードの考え方」の発表資料になります。
https://trident-qa.connpass.com/event/320151/

h.uriu

June 25, 2024
Tweet

Other Decks in Programming

Transcript

  1. 自己紹介 瓜生 遥輝 / Uriu Haruki 株式会社イーディーエー / Flutter Engineer

    Qiitaアカウント: https://qiita.com/haru-qiita 趣味: フットサルとサッカー観戦
  2. my_project/ ├── lib/ │ ├── models/ │ │ └── user.dart

    │ ├── services/ │ │ └── user_service.dart │ └── utils/ │ └── string_utils.dart └── test/ ├── models/ │ └── user_test.dart ├── services/ │ └── user_service_test.dart └── utils/ └── string_utils_test.dart
  3. $ flutter test 00:02 +3: All tests passed! 実装が完了した後は、flutter test

    をターミナルで 実行して、テスト結果を確認します。 00:02 : テストが実行された総時間 +3 : テストが成功したことを 示す値、成功であれば`+` 失敗した 場合は `-` が表示されます。 All tests passed! : すべてのテストが成功したというメッ セージです。 プロジェクト内のすべてのテストケース が正常に実行されていることを表して います。 失敗 成功
  4. 基本メソッド • testOn • timeout • skip • tags •

    onPlatform • retry test() expect() その他 • reason • skip • matcher • group() • setUp() • setUpAll() • tearDown() • tearDownAll()
  5. test() テストケースを定義するためのメソッドです test('subtract', () { // Arrange const a =

    8; const b = 3; // Act final result = mathOperations.subtract(a, b); // Assert expect(result, equals(5)); }); } body description 必須のプロパティは、以下の2つになります。 • description: テストケースの説明を文字列で指定します。 この説明はテスト結果の表示やテスト実行時のログに 使用されます • body: テストの本体であるテストコードを記述します。
  6. matcher メソッド 機能 equals マッチャー 2つのオブジェクトが等しいことを確認します。 isTrue マッチャー 真偽値が true

    であることを確認します。 isFalse マッチャー 真偽値が false であることを確認します。 isNull マッチャー 値が null であることを確認します。 contains マッチャー コレクションが指定した要素を含んでいることを確認します。 isA マッチャー オブジェクトが指定した型であることを確認します。 throwsA マッチャー 指定した例外をスローすることを確認します。 hasLength マッチャー コレクションが指定した長さであることを確認します。
  7. // equals マッチャー test('equals sample', () { int actual =

    5; int expected = 5; expect(actual, equals(expected)); }); // isTrue・isFalse マッチャー test('isTrue・isFalse sample', () { bool condition = true; expect(condition, isTrue); }); // isNull マッチャー test('isNull sample', () { String? value; expect(value, isNull); }); // contains マッチャー test('contains sample', () { List<int> numbers = [1, 2, 3, 4, 5]; int value = 3; expect(numbers, contains(value)); });
  8. isA マッチャー オブジェクトが指定した型で あることを確認します。 // isA マッチャー test('isA sample', ()

    { expect('Hello', isA<String>()); }); throwsA マッチャー 指定した例外をスローすることを確認 します。 // throwsA マッチャー test('throwsA sample', () { expect(() => throw Exception('TestException'), throwsA(const TypeMatcher<Exception>())); }); // hasLength マッチャー test('hasLength sample', () { expect('Hello', hasLength(5)); }); // isEmpty マッチャー test('isEmpty sample', () { List<int> emptyList = []; expect(emptyList, isEmpty); });
  9. import 'package:flutter_test/flutter_test.dart'; import 'package:tester/main.dart'; void main() { late MathOperations mathOperations;

    setUp(() { mathOperations = MathOperations(); }); test('add', () { // Arrange const a = 5; const b = 3; // Act final result = mathOperations.add(a, b); // Assert expect(result, equals(8)); }); test('subtract', () { // Arrange const a = 8; const b = 3; // Act final result = mathOperations.subtract(a, b); // Assert expect(result, equals(5)); }); }
  10. メソッド 機能 find.text 特定のテキストを持つウィジェットを検索します find.byType 指定されたタイプ(クラス)のウィジェットを検索します find.byKey 特定のキーを持つウィジェットを検索します find.byIcon 特定のアイコンを持つウィジェットを検索します

    find.byWidget 特定のウィジェットインスタンスを検索します find.byTooltip 特定のツールチップテキストを持つウィジェットを検索します Finderメソッド Finderメソッドとは、テスト環境でレンダリングされたWidgetツリーの中から特定の Widgetを見つけるためのメソッドです。
  11. testWidgets('Counter increments Widget test', (WidgetTester tester) async { // Arrange:

    テストデータの準備 await tester.pumpWidget(MyApp()); // 初期状態の検証 expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Act: テストの実行 await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Assert: テスト結果の検証 expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); 1. Arrange(準備) テスト対象のウィジェットをレンダリングし、 初期状態を設定します。 2. Act(実行) tester.tapメソッドを使って、フローティングアク ションボタン(FAB)をタップし、その後 tester.pump メソッドを使ってウィジェットツリーを再描画します。 3. Assert(検証) 期待される結果が得られているかを確認します。 expectメソッドを使って、テキスト '0'が見つからず、 テキスト'1'が見つかることを確認します。
  12. 単一責任原則 (Single Responsibility Principle) とは Robert C. Martinの定義  "Every software

    component should have one and only one responsibility." 各クラスや関数が一つの責任を持つ状態 理解しやすい && 保守しやすいコード
  13. class UserManager { final UserRepository _userRepository; final AnalyticsService _analyticsService; UserManager(this._userRepository,

    this._analyticsService); Future<void> signIn(String username, String password) async { User user = await _userRepository.signIn(username, password); _analyticsService.trackSignIn(user.id); } Future<void> signOut() async { await _userRepository.signOut(); _analyticsService.trackSignOut(); } } <問題点> ・UserManager クラスが UserRepository と AnalyticsService の両方の責任を持っている。 ・signIn と signOut のテストが難しくなる。 SRPが遵守されていないコード
  14. class AuthService { final UserRepository _userRepository; AuthService(this._userRepository); Future<User> signIn(String username,

    String password) { return _userRepository.signIn(username, password); } Future<void> signOut() { return _userRepository.signOut(); } } class AnalyticsManager { final AnalyticsService _analyticsService; AnalyticsManager(this._analyticsService); void trackSignIn(String userId) { _analyticsService.trackSignIn(userId); } void trackSignOut() { _analyticsService.trackSignOut(); } } <改善点> ・AuthService と AnalyticsManager という 2つのクラスに分割。 ・それぞれのクラスが単一の責任を持つ。 SRPが遵守されたコード
  15. まとめ 01. Unitテストの基本  - 単一の関数やメソッドをテストする  - 入力値と期待する出力値を用意する Arrange(準備)、Act(実行)、Assert(確認)の3ステップ  - matcherを用いてAssert

    (確認) を実施する 03. 単一責任に焦点を当てる  - 1つのテストケースで1つの振る舞いのみをテストする  - テストケースが複数の責任を持つと、保守性が低下する  - テストの意図が明確になり、他の開発者が理解しやすくなる 02. Widgetテストを学ぶ  - 他のテスト(UnitTest、IntegrationTest)と比較して、UIにフォーカスしたテスト  - Widgetテストとは、個々のウィジェットのレイアウトや動作を検証するテスト  - Finderとmatcherを使用してウィジェットの存在と状態を確認