使用 TDD + Clean Architecture 開發 Flutter 專案課程筆記 1 ~ 3

hands typing on a laptop keyboard Flutter
Photo by cottonbro studio on Pexels.com

前言

以前專職開發 Android 專案時,我喜愛 Clean Architecture + MVVM 架構當作基本配置;TDD 一直是我很想精通的一個技能,但礙於各種因素,正式工作中我一向是在最後階段才補上測試。這次學習 Flutter 課程,除了想將技能樹向外延伸,也想複習架構,並順便習慣 TDD。

課程

本次課程選用 Matt Rešetár 的 Flutter TDD Clean Architecture Course:

這個課程適合熟悉 Flutter 或是熟悉 TDD 及 Clean Architecture 的開發者,若沒有基礎就直接來上,可能資訊量會太大;以我來說,我習慣使用 Clean Architecture 且接觸過 TDD,有疑惑的部分就會是 Flutter / Dart 為主。

筆記

以下為我觀看教學影片的筆記,並不會完整記載影片重點,但會針對我不熟悉的內容來留下紀錄,可能無法滿足所有人,可以斟酌瀏覽。

Flutter TDD Clean Architecture Course [1] – Explanation & Project Structure

第一集主要在介紹 Clean Architecture,教學專案結構,雖然作者是用 VS Code 進行開發,但對 Android Studio 開發者並無障礙。

配置 Clean Architecture 的專案架構

Flutter TDD Clean Architecture Course [2] – Entities & Use Cases

第二集開頭貼了這個專案會用到的 dependency libs,很多 lib 的版本號可能都太舊了,為了避免無法進行後續的課程,就先維持他貼的版本。

class NumberTrivia extends Equatable {
  final String text;
  final int number;
  NumberTrivia({
    @required this.text,
    @required this.number,
  }) : super([text, number]);
}

在新增第一個 entity 時,有用到 equatable 以及 meta 兩個 package,為了使用 equatable,必須將 class member 都丟給父類別,這樣才有辦法比較,不用 override equals,滿方便的。至於 meta,當然就是增加專案的可分析性、可讀性。

abstract class NumberTriviaRepository {
  Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
  Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}

講到 error handling 時,作者提到 dartz 是用來處理回傳二選一 class 的 package,教學中寫成 Either<FailureModel, SuccessModel>。此外,Future 是用來處理非同步的 class。

在開始寫測試之前,要先將測試的目錄結構產出,跟上面一開始的專案結構一樣,只是要新增在 test 目錄之下。
作者寫測試時,有用一個快速生成測試的 template 叫做 aaaTest,Android Studio 也有支援這個功能,叫做 Live Template,只要去 Preference -> Editor -> Flutter 底下新增 aaaTest 即可。
void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;
  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });
  final tNumber = 1;
  final tNumberTrivia = NumberTrivia(number: tNumber, text: "test");
  test(
    'should get trivia for the number from the repository',
    () async {
      // arrange
      when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
          .thenAnswer((_) async => Right(tNumberTrivia));
      // act
      final result = await usecase.execute(number: tNumber);
      // assert
      expect(result, tNumberTrivia);
      verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
    },
  );
}

setUp() 在執行每一個單一測試之前都會跑過一遍,在 Android 單元測試也有一樣的 method。

作者在測試中採用 mockito 來 mock 所有物件,值得注意的是:因為要測非同步的回應,所以不能用 .thenReturn(),而是要使用 .thenAnwser() 來處理。

verify() 是為了確認 mock 物件有沒有確實呼叫函式的 mockito method。

Flutter TDD Clean Architecture Course [3] – Domain Layer Refactoring

// before
Future<Either<Failure, NumberTrivia>> execute({@required int number}) async {
    return await repository.getConcreteNumberTrivia(number);
}
final result = await usecase.execute(number: tNumber);
// after
Future<Either<Failure, NumberTrivia>> call({@required int number}) async {
    return await repository.getConcreteNumberTrivia(number);
}
final result = await usecase(number: tNumber);

影片一開始就做了一個重構,將 use case 的 execute method rename 為 call,這時神奇的事情發生了,他也沒去呼叫 usecase.call(),就只留下 usecase();原來 Dart 提供 callable class,實作方法就是直接新增一個 call(),就可以將 class 當作 function 來使用,這個用法等同於 Kotlin 的 operator fun invoke()。

接著是基礎建設,將 use case 抽出一個 base class,主要是想讓所有 use case 統一標準,避免不同的 use case 有不同的使用方法,理由滿好笑但也很真實:工程師會忘記。

影片後期就是依樣畫葫蘆再新增一個 use case,這集主要就是 use case 的收尾。

專案連結

想看我實作的完整專案請瀏覽我的 github:

留言列表

Copied title and URL