LoginSignup
4

More than 1 year has passed since last update.

FlutterでClean ArchitectureとTDDをやってみる その2 - EntityとUseCase

Last updated at Posted at 2020-12-16

どこからコーディングを始めるか

  • コーディングは内側から始める
    • 最も変化しないDomain層のEntityから始める

Entity

  • Number Triviaアプリが扱うデータはNumberTrivia entitiyである。
  • EntityはTestしない
    • 自明であるし変化しないためだと思われる
  • 値の比較のためにEquatableを継承する

Use Cases

  • ユースケースではビジネスロジックが実行される
    • Repositoryからデータを得る
  • このアプリでは2つのユースケースを用いる
    • GetConcreteNumberTrivia
    • GetRandomNumberTrivia

データフローとエラーハンドリング

  • UseCaseとRepositoryで返す2つの型

    • NumberTrivia Entity
      • UseCaseはRepositoryからNumberTrivia Entityを受け取り、Presentation層に渡す
    • Error
      • できるだけ早い段階(Repositoryの段階)で例外をcatchしてFailureオブジェクトを返す方法をとる
  • 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のインターフェイスのみ作成する。
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の実装には依存せず、インターフェイスに依存する
  • Repositoryの実装が存在しない状態でUseCaseを実装するには、Repositoryのインターフェイスとmockを用いたTDDが自然な手法となる
    • つまり、CleanArchitectureをしようとするとTDDするのが1つの自然な方法となる。
    • TDD以外だとインターフェイスを継承したFakeClassを作って実装をすすめることがある
  • テストコードは3段階になる
    • 事前準備: mock等の設定
    • 実行: テスト対象メソッドを実行
    • 検証: メソッドの実行結果を検証

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4