[Flutter] : TDD - 08. Local Data Sources


Local Data Base는 앱에 기본적으로 있는 Shared_preferences로 진행한다.

Local DB에는 수많은 옵션들이 있지만

우리는 NumberTrivia 1개를 저장하기 때문에 굳이 다른 것을 쓰지 않고 Shared_preferences를 사용하기로 했다.

Test


자 TDD 원칙 대로 먼저 test 코드를 작성하자.

class MockSharedPreferences extends Mock implements SharedPreferences {}

void main() {
  NumberTriviaLocalDataSourceImpl dataSource;
  MockSharedPreferences mockSharedPreferences;

  setUp(() {
    mockSharedPreferences = MockSharedPreferences();
    dataSource = NumberTriviaLocalDataSourceImpl(
        sharedPreferences: mockSharedPreferences);
  });


}

implementation


number_trivia_local_data_source.dart

abstract class NumberTriviaLocalDataSource {
  //cache 데이터를 꺼내간다.
  Future<NumberTriviaModel> getLastNumberTrivia();

  //cache 데이터를 업데이트 한다.
  Future<void> cacheNumberTrivia(NumberTriviaModel tiriviaToCahce);
}

class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
  final SharedPreferences sharedPreferences;

  NumberTriviaLocalDataSourceImpl({@required this.sharedPreferences});

  @override
  Future<void> cacheNumberTrivia(NumberTriviaModel tiriviaToCahce) {
    // TODO: implement cacheNumberTrivia
    throw null;
  }

  @override
  Future<NumberTriviaModel> getLastNumberTrivia() {
    // TODO: implement getLastNumberTrivia
    throw null;
  }
}

여기서 구현하기 전에 우리가 생각 해봐야 할게 있다.

Shared_Preferences는 String만 저장가능하다.

그렇다면 우리 NumberTrivia 데이터를 어떻게 저장하면 좋을까?

JSON 으로 String화 해서 저장하자.

이건 전에 fixtures 폴더에 구현을 해놓았다.

number_trivia_model.dart

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

number trivia model에도 toJson이 구현이 되어있다.

local_data_source 테스트를 위해 test 폴더 fixtures 폴더 아래

tirivia_cached.json 파일을 만들어주자.

/test/fixtures/trivia_cached.json

{
  "text": "Test Text",
  "number": 1
}

getLastNumberTrivia


이제 getLastNumberTrivia 함수를 TDD로 개발해보자.

먼저 Test코드다.

number_trivia_local_data_source_test.dart

group('getLastNumberTrivia', () {
    final tNumberTriviaModel =
        NumberTriviaModel.fromJson(json.decode(fixture('trivia_cached.json')));

    test(
        'should return NumberTrivia from SharedPreferences when there is one in the cache',
        () async {
      // arrange
      when(mockSharedPreferences.getString(any))
          .thenReturn(fixture('trivia_cached.json'));
      // act
      final result = await dataSource.getLastNumberTrivia();
      // assert
      verify(mockSharedPreferences.getString('CACHED_NUMBER_TRIVIA'));
      expect(result, equals(tNumberTriviaModel));
    });
  });

이제 test코드를 작성 했으니 test가 돌아가게 구현해보자.

number_trivia_local_data_source.dart

@override
  Future<NumberTriviaModel> getLastNumberTrivia() {
    final jsonString = sharedPreferences.getString('CACHED_NUMBER_TRIVIA');
    // future는 데이터 반환 타입을 맞춰주기 위해 사용
    return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
  }

위의 데이터 반환 형식과 맞추기 위해 Future.value()를 사용하였다.

그리고 SharedPreferences에서 Key값으로 CACHED_NUMBER_TRIVIA를 사용하는데

Const화 해서 사용하자.

const CACHED_NUMBER_TRIVIA = 'CACHED_NUMBER_TRIVIA';

CacheException


이제 cache 데이터가 없어서 생기는 오류 상황을 처리해보자.

number_trivia_local_data_source_test.dart

test('should throw a CacheException when there is not a cached value', () {
      // arrange
      when(mockSharedPreferences.getString(any)).thenReturn(null);
      // act
      final call = dataSource.getLastNumberTrivia;
      // assert
      expect(() => call(), throwsA(isInstanceOf<CacheException>()));
    });

여기서 보면 expect에 평상시랑 다르게 함수나 value를 넣지 않고 Lambda 식으로 함수를 호출 하는 걸 볼 수 있는데 왜냐하면 함수를 실행하는 도중 Exception을 Throw 하기 때문에

call에 value를 넣는게 아니라 function을 넣어서

expect로 함수 호출 도중에 throw를 감지하게 했다.

impl.dart

@override
  Future<NumberTriviaModel> getLastNumberTrivia() {
    final jsonString = sharedPreferences.getString(CACHED_NUMBER_TRIVIA);
    if (jsonString != null) {
      // future는 데이터 반환 타입을 맞춰주기 위해 사용
      return Future.value(NumberTriviaModel.fromJson(json.decode(jsonString)));
    } else {
      throw CacheException();
    }
  }

cacheNumberTrivia


이제 local_data_source의 2번째 함수인 cacheNumberTrivia를 구현해보자.

먼저 테스트코드다.

test.dart

group('cacheNumberTrivia', () {
    final tNumberTriviaModel =
        NumberTriviaModel(number: 1, text: "test trivia");

    test('should call SharedPreferences to cache the data', () {
      // act
      dataSource.cacheNumberTrivia(tNumberTriviaModel);
      // assert
      final expectedJsonString = json.encode(tNumberTriviaModel.toJson());
      verify(mockSharedPreferences.setString(
          CACHED_NUMBER_TRIVIA, expectedJsonString));
    });
  });

impl.dart

@override
  Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCahce) {
    return sharedPreferences.setString(
        CACHED_NUMBER_TRIVIA, json.encode(triviaToCahce.toJson()));
  }

자 테스트도 정상적으로 수행된다.

다음은 Remote data source를 test 및 구현해보자.