[Flutter] : TDD - 04.Domain Layer Overview & Models


우리는 이전에 Domain Layer를 구현했고 그중에서 Entity를 만들었습니다.

이제 Model부터 만들고 그다음 Repository와 DataSource를 구현하려고 합니다.

이 순서는 Clean Architecture에서 말하는 내부 레이어부터 진행하는 그림입니다.

Model


Repository와 다르게 DataSource는 비슷한 것 같지만 큰 차이가 있습니다.

Repository는 Entity를 DataSource는 Model을 반환합니다.

추가로 Failures를 정의해 에러 처리를 하는지 아니면 exception처리를 하는지 차이도 있습니다.

이미지

Model은 Entity를 상속해 JSON 혹은 XML 데이터를 파싱하는 로직을 만들어 줍니다.

나중에 JSON에서 XML로 구조가 변경되어도 비즈니스 룰의 핵심인 Domain Layer를 건드리지 않고

Data Layer에서 Model 로직을 수정 또는 추가 함으로써 대응 할 수 있습니다.

이제 Model을 만들어봅시다.

먼저 우리는 TDD를 진행중 임을 까먹지 말고 먼저 Test코드를 작성해줍시다.

test/features/number_trivia/data/models/number_trivia_model_test.dart

void main() {
  final tNumberTriviaModel = NumberTriviaModel(number: 1, text: 'Test Text');

  test('should be a subclass of NumberTirivia entity', () async {
    //assert
    expect(tNumberTriviaModel, isA<NumberTrivia>());
  });
}

위의 코드는 아직 NumberTirivialModel이 작성되지 않았기에 오류가 발생하지만 먼저 Test를 정의해줍니다.

isA() 으로 TypeMatcher를 진행할 수 있습니다.

이제 Model 코드를 작성합니다.

class NumberTriviaModel extends NumberTrivia {
  NumberTriviaModel({
    @required String text,
    @required int number,
  }) : super(text: text, number: number);
}

그리고 Test를 돌려보면 아주 잘 작동하는 걸 확인 할 수 있습니다.

fromJson


이제 Json 파일을 가져와 테스트 코드를 작성해보자.

먼저 test 폴더에 test할 json 파일을 만들어주자. (나중에 datasource로 직접 가져옴 API)

test/fixtures/trivia.json

{
  "text": "Test Text",
  "number": 1,
  "found": true,
  "type": "trivia"
}

test/fixtures/trivia_double.json

{
  "text": "13.12 is the test number.",
  "number": 13.12,
  "found": true,
  "type": "trivia"
}

이렇게 가장 기본적인 Integer형태 말고도 double으로 들어오는 형태 처리 test문을 만들자.

numberAPI 에서 double형이 넘어 오기도 하기 때문에 test json을 이렇게 만들었다.

test에서 json을 읽을 수 있게 다음 코드를 작성해주자.

test/fixtures/fixture_reader.dart

import 'dart:io';

String fixture(String name) => File('test/fixtures/$name').readAsStringSync();

아까 작성했던 model_test.dart에 추가로 작성하자.

test/features/number_trivia/data/models/number_trivia_model_test.dart

void main() {
	...

  group('fromJson', () {
    test('should return a valid model when the Json number is an integer',
        () async {
      //arrange
      final Map<String, dynamic> jsonMap = json.decode(fixture('trivia.json'));
      //act
      final result = NumberTriviaModel.fromJson(jsonMap);
      //assert
      expect(result, tNumberTriviaModel);
    });
  });
}

현재 NumberTriviaModel의 fromJson이 작성이 안되있다. 이제 Test코드를 작성하였으니 로직을 완성하러 가보자.

class NumberTriviaModel extends NumberTrivia {
  NumberTriviaModel({
    @required String text,
    @required int number,
  }) : super(text: text, number: number);

  factory NumberTriviaModel.fromJson(Map<String, dynamic> json) {
    return NumberTriviaModel(number: json['number'], text: json['text']);
  }
}

간단하게 구현하였다.

이제 Test를 돌려보면 정상적으로 동작한다.

추가로 double형일 때를 돌려보자.

Test코드를 아래 group에 이어서 추가 해주자. (Group으로 Test를 묶어서 편하게 볼 수 있다.)

group('fromJson', () {
    test('should return a valid model when the Json number is an integer',
        () async {
      //arrange
      final Map<String, dynamic> jsonMap = json.decode(fixture('trivia.json'));
      //act
      final result = NumberTriviaModel.fromJson(jsonMap);
      //assert
      expect(result, tNumberTriviaModel);
    });

    test('should return a valid model when the Json number is regarded as a double',
        () async {
      //arrange
      final Map<String, dynamic> jsonMap = json.decode(fixture('trivia_double.json'));
      //act
      final result = NumberTriviaModel.fromJson(jsonMap);
      //assert
      expect(result, tNumberTriviaModel);
    });
  });

Test를 돌려보면 Error가 발생하는걸 확인할 수 있다.

Error내용을 봐보자

fromJson should return a valid model when the Json number is regarded as a double:

ERROR: type 'double' is not a subtype of type 'int'
package:number_trivia/features/number_trivia/data/models/number_trivia_model.dart 11:42  new NumberTriviaModel.fromJson
test/features/number_trivia/data/models/number_trivia_model_test.dart 33:40              main.<fn>.<fn>

Type이 안맞아서 생기는 오류이다.

에러가 발생한 부분을 위의 내용대로 찾아가면 아래 부분이다.

factory NumberTriviaModel.fromJson(Map<String, dynamic> json) {
    return NumberTriviaModel(number: json['number'], text: json['text']);
  }

여기서 dynamic으로 들어온 number 값이 double일수도 있는데 그냥 넣어 주니 Casting Error가 발생했다.

아주 간단하게 해결 해보자.

factory NumberTriviaModel.fromJson(Map<String, dynamic> json) {
    return NumberTriviaModel(number: (json['number'] as num).toInt(), text: json['text']);
  }

as num ⇒ num으로 변환시켜서 Int로 다시 Casting했다.

toJson


이제 toJson test코드를 작성해보자.

group('toJson', () {
    test('should return a JSON map containing the proper data', () async {
      //arrange
      final result = tNumberTriviaModel.toJson();
      //assert
      final expectedMap = {
        "text": "Test Text",
        "number": 1,
      };
      expect(result, expectedMap);
    });
  });

toJson 함수를 구현해보자.

class NumberTriviaModel extends NumberTrivia {
  ...

  Map<String, dynamic> toJson() {
    return {
      'text': text,
      'number': number,
    };
  }
}

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b817326a-c7be-4e3b-9c3d-6cdb6c724e6c/_2020-09-01__4.50.45.png

초록 불 마음이 평온하다.