- Flutter TDD Clean Architecture Course 2 を解説します。
- コードは該当ページのGitHubリンクから取得するのが良いと思います。
- チュートリアルを読むのが早いと思いますが、その前に概要をこちらにまとめたので呼んでいただいてもいいかなと思います。
どこからコーディングを始めるか
- コーディングは内側から始める
- 最も変化しないDomain層のEntityから始める
Entity
- Number Triviaアプリが扱うデータはNumberTrivia entitiyである。
-
NumberTrivia APIからのレスポンスを基に作成する
- レスポンスの例をhttp://numbersapi.com/#42から得る。
- String text と int number をプロパティとする
- not_foundのレスポンスをhttp://numbersapi.com/123456?jsonから得る
- レスポンスの例をhttp://numbersapi.com/#42から得る。
-
NumberTrivia APIからのレスポンスを基に作成する
- EntityはTestしない
- 自明であるし変化しないためだと思われる
- 値の比較のためにEquatableを継承する
Use Cases
- ユースケースではビジネスロジックが実行される
- Repositoryからデータを得る
- このアプリでは2つのユースケースを用いる
- GetConcreteNumberTrivia
- GetRandomNumberTrivia
データフローとエラーハンドリング
-
UseCaseとRepositoryで返す2つの型
- NumberTrivia Entity
- UseCaseはRepositoryからNumberTrivia Entityを受け取り、Presentation層に渡す
- Error
- できるだけ早い段階(Repositoryの段階)で例外をcatchしてFailureオブジェクトを返す方法をとる
- NumberTrivia Entity
-
Either: EntityとFailureの2つの型を扱う方法
- dartz packageを利用する
-
Repositoryが返すデータ型は
Future<Either<Failure, NumberTrivia>>
となる
Repository Contract
- Use CasesはRepositoryからデータを取得する
- Repositoryはdomain層とdata層に位置する
- Contract(インターフェイス)がdomain層に位置する
- 実装がdata層に位置する
- これにより依存性逆転しdomain層の独立が保てる
- また、testabilityが得られる
- testabilityと分離が良い設計となる
- Repositoryはdomain層とdata層に位置する
- Repositoryのインターフェイスのみ作成する。
number_trivia_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';
abstract class NumberTriviaRepository {
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number);
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia();
}
モックによるテスト
- 実装がないクラスを用いるテストにはモック使う
- mockitoパッケージを使う
TDD
- 先にテストを書く
-
/test/usecases/get_concrete_number_trivia_test.dart
を作成する。これがテストファイルになる -
/lib/domain/usecases/get_concrete_number_trivia.dart
を作成する
-
テストを作成して失敗させる
- この段階でUseCaseで用いるRepositoryのインターフェイスは作成済みであるが、実装はまだ作成していない
- Repositoryのインターフェイスを継承するmockを使いテストする
- 以下のテストは
GetConcreteNumberTrivia
がないため失敗する
get_concrete_number_trivia_test.dart
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
// NumberTriviaRepository(インターフェイス)を用いてmockを作成する
class MockNumberTriviaRepository extends Mock
implements NumberTriviaRepository {}
void main() {
// GetConcreteNumberTrivia は存在しないためErrorとなる
GetConcreteNumberTrivia usecase;
MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
});
}
失敗したテストを成功させるためにusecaseを作成する
get_concrete_number_trivia.dart
import '../repositories/number_trivia_repository.dart';
class GetConcreteNumberTrivia {
final NumberTriviaRepository repository;
GetConcreteNumberTrivia(this.repository);
}
テスト本体を書く
get_concrete_number_trivia_test.dart
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:clean_architecture_tdd_prep/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockNumberTriviaRepository extends Mock
implements NumberTriviaRepository {}
void main() {
GetConcreteNumberTrivia usecase;
MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
});
final tNumber = 1;
final tNumberTrivia = NumberTrivia(number: 1, text: 'test');
test(
'should get trivia for the number from the repository',
() async {
// Mockを用いたテスト
// getConcreteNumberTrivia が呼ばれた際は常にRight(=成功)側のNumberTriviaオブジェクトを返すようにする
when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
.thenAnswer((_) async => Right(tNumberTrivia));
// テストの実行のコード。まだ書いていないメソッドに対してテストをする = 失敗させる
final result = await usecase.execute(number: tNumber);
// UseCaseは単純にRepositoryが返す値を返す
expect(result, Right(tNumberTrivia));
// Respositoryでメソッドが呼ばれたことを検証する
verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
// Respositoryでメソッドがそれ以降呼ばれないことを検証する
verifyNoMoreInteractions(mockNumberTriviaRepository);
},
);
}
テストが成功するような実装を書く
get_concrete_number_trivia.dart
import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';
import '../../../../core/error/failure.dart';
import '../entities/number_trivia.dart';
import '../repositories/number_trivia_repository.dart';
class GetConcreteNumberTrivia {
final NumberTriviaRepository repository;
GetConcreteNumberTrivia(this.repository);
Future<Either<Failure, NumberTrivia>> execute({
@required int number,
}) async {
return await repository.getConcreteNumberTrivia(number);
}
}
まとめと感想
Clean Architectureの中央からコードを書く
- 変更しないものから書くという考え方になる
- 外側から内側に依存する設計なので、内側から順に作成できる
- ただし、UseCaseは外側のRepositoryからデータを受け取る。CleanArchitectureはこれを依存性逆転の原則で解決している
- UseCaseはRepositoryの実装には依存せず、インターフェイスに依存する
- ただし、UseCaseは外側のRepositoryからデータを受け取る。CleanArchitectureはこれを依存性逆転の原則で解決している
-
Repositoryの実装が存在しない状態でUseCaseを実装するには、Repositoryのインターフェイスとmockを用いたTDDが自然な手法となる
- つまり、CleanArchitectureをしようとするとTDDするのが1つの自然な方法となる。
- TDD以外だとインターフェイスを継承したFakeClassを作って実装をすすめることがある
- テストコードは3段階になる
- 事前準備: mock等の設定
- 実行: テスト対象メソッドを実行
- 検証: メソッドの実行結果を検証