5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

わたしのFlutterお勉強用Advent Calendar 2020

Day 16

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等の設定
    • 実行: テスト対象メソッドを実行
    • 検証: メソッドの実行結果を検証
5
4
0

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
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?