[Flutter] : TDD - 02.Domain


다음은 여기서 사용할 라이브러리 들이다.

dependencies:
  flutter:
    sdk: flutter
  # Service locator
  get_it: ^2.0.1
  # Bloc for state management
  flutter_bloc: ^0.21.0
  # Value equality
  equatable: ^0.4.0
  # Functional programming thingies
  dartz: ^0.8.6
  # Remote API
  connectivity: ^0.4.3+7
  http: ^0.12.0+2
  # Local cache
  shared_preferences: ^0.5.3+4

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^4.1.0

Domain


domain에는 3가지 영역이 있다.

  • Entity : 데이터 객체이다.
  • Usecase : 비즈니스 로직이다.
  • Repository : 데이터를 가져오는 통로이다.

Entity


NumberTrivia App에서는 API 통신을 통해 다음과 같은 구조의 Json 데이터를 가져온다.

{
  "text": "42 is the answer to the Ultimate Question of Life, the Universe, and Everything.",
  "number": 42,
  "found": true,
  "type": "trivia"
}

Entities 폴더 안에 number_trivia.dart 파일을 위의 형식에 맞춰서 만들자

유일하게 Entites만 test 코드 없이 진행된다. test할 것이 없다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

class NumberTrivia extends Equatable {
  final String text;
  final int number;

  NumberTrivia({@required this.text, @required this.number})
      : super([text, number]);
}

Equatable은 내부 값들로 동등한지 비교해주는 라이브러리다. super([text, number])를 통해 text와 number 값을 비교해 동등한지 확인한다.

Usecase


Usecase 는 비즈니스 로직이 실행되는 곳입니다. 물론 Number Trivia는 간단한 앱이다.

GetConcreteNumberTrivia, GetRandomNumberTrivia 2가지만 있다.

데이터 흐름과 오류 처리 흐름을 확인해보자.

이미지

위의 이미지를 보면 Repositories에서 Number Trivia Entity를 가져와 Use CasePresentation으로 넘겨준다.

여기서 UsecaseEntityFuture<NumberTrivia> 형태로 받아와야 한다. (비동기 처리)

  • 이때 에러 처리는 어떻게 할 것인가?

    최대한 빠르게 파악해서 오류를 반환하는 구조가 좋다.

    Repository에서 예외를 파악해 Failure를 보내주는 방식으로 처리하자.

여기서 우리는 함수형 프로그래밍이 필요하다

  • dartz package

위의 패키지를 통해 함수평 프로그래밍을 사용할 수 있게 되었다.

단, 함수형 프로그래밍을 깊게 사용하거나 사전 지식이 필요한건 아니다.

Either<L, R> 이것만 사용할 것이다.

Either<L, R>에서 L,R은 제네릭이며 다음과 같다.

  • L : 실패했을 때 반환
  • R : 성공했을 때 반환

오류 정의


core 폴더 아래 error 폴더를 만들어 Failure 클래스부터 작성해주자.

//faulures.dart
import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {
  // 하위 클래스에 properties가 있으면 생성자를 통해
  // Equatable의 비교가 동작한다.
  Failure([List properties = const <dynamic>[]]) : super(properties);
}

이제 이 failure를 활용해 ServierFailure등을 만들어 줄 수 있다.

Repository Contract


그전 전체 구조도를 보면 Repositories가 Domain과 Data 영역 모두에 포함되있었다.

종속성 역전을 위해서 다음과 같이 구성되어 있다.

  • Definition(추상 클래스) : Domain 영역에서 진행
  • implementation(클래스 구현) : Data 영역에서 진행

이렇게 하면 Domain 영역과 완전히 독립성을 허용하지만 더 큰 이점은 Testability에 있다.

이제 repository의 추상클래스를 작성해보자.

//  ...fatures/number_trivia/domain/repositories/number_tirivia_repository.dart
import 'package:dartz/dartz.dart';
import 'package:number_trivia/core/error/failures.dart';
import 'package:number_trivia/features/number_trivia/domain/entities/number_tirivia.dart';

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

Dart의 경우 추상 클래스인 Repository abstract class 를 작성하면 Repository 구현 없이 Usecase에 대한 Test 코드를 작성 할 수 있습니다. 이것에 대한 인기있는 패키지는 mockito , dev_dependency에 추가해줍니다.

Mock Test


TDD의 경우 프로덕션 코드를 작성하기 전에 테스트 코드를 작성합니다.

이것은 우리가 필요하지 않은 것들을 추가하지 않고 우리 코드가 스파게티가 되지 않도록 막아줍니다.

test 폴더 내부 구조를 다음과 같이 해줍니다.

test코드는 lib 폴더 파일들과 매핑하여 진행됩니다.

이미지

이제 GetConcreteNumberTriviaTest를 작성해보자.

// test/domain/usecases/get_concrete_number_trivia_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:number_trivia/features/number_trivia/domain/repositories/number_trivia_repository.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });
}

아직 테스트 코드를 작성하지 않았지만 위의 설정 코드에 오류가 없도록

GetConcreteNumberTrivia 클래스의 골격을 만들자. (에러가 싫다)

import 'package:number_trivia/features/number_trivia/domain/repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);
}

이제 실제 테스트 코드를 작성해보자.

단순한 앱이라 복잡하지 않다. (실제로 데이터를 가져오는 것 말고는 아무것도 없다)

따라서 데이터를 Repository에서 호출하고 데이터를 usecase가 변경되지 않은 상태로 전달 하는지 확인하자.

import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:number_trivia/features/number_trivia/domain/entities/number_tirivia.dart';
import 'package:number_trivia/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:number_trivia/features/number_trivia/domain/usecases/get_concrete_number_trivia.dart';

class MockNumberTriviaRepository extends Mock
    implements NumberTriviaRepository {}

void main() {
  GetConcreteNumberTrivia usecase;
  MockNumberTriviaRepository mockNumberTriviaRepository;

  setUp(() {
    mockNumberTriviaRepository = MockNumberTriviaRepository();
    usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
  });

  //test 코드 시작
  final tNumber = 1;
  final tNumberTirivia = NumberTrivia(text: "test", number: 1);

  test('should get tirivia for the number from the repository', () async {
    // arrange
    when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
        .thenAnswer((_) async => Right(tNumberTirivia));
    // act , 아직 구현되지 않음 함수
    final result = usecase.excute(number: tNumber);
    // assert
    expect(result, Right(tNumberTirivia));
    // Repository에서 함수가 호출되었는지 확인
    verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
    // 위의 방법만 호출하고 더 이상 호출하면 안된다.
    verifyNoMoreInteractions(mockNumberTriviaRepository);
  });
}

아직 excute함수가 구현되지 않아 Test할 수 없다. 구현하고 와보자.

import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';
import 'package:number_trivia/core/error/failures.dart';
import 'package:number_trivia/features/number_trivia/domain/entities/number_tirivia.dart';
import 'package:number_trivia/features/number_trivia/domain/repositories/number_trivia_repository.dart';

class GetConcreteNumberTrivia {
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);

  Future<Either<Failure, NumberTrivia>> excute({@required int number}) async {
    return await repository.getConcreteNumberTrivia(number);
  }
}

테스트용으로 usecase를 구현하였다. 이제 test를 돌려보자.

이미지

아주 정상적으로 test를 통과하였다.