Flutter TDD Clean Architecture Course その11, 12 - Blocの実装

- [こちらのチュートリアル（11）](https://resocoder.com/2019/10/10/flutter-tdd-clean-architecture-course-11-bloc-implementation/)と[（12）](https://resocoder.com/2019/10/14/flutter-tdd-clean-architecture-course-11-bloc-implementation-2-2/)の解説をします。
- チュートリアルを読むのが早いと思いますが、[概要をまとめた](https://qiita.com/maria_mari/items/8fdd92af13a36c663360)のでこちらもご参照ください。

---

- Blocや他のPresentation logic holderは、Domain層とPresentation層の間に位置する

```
UseCase -> Presentation Logic Holders -> Widgets
```

# Event-Drivenテスト
- `mapEventToState()`にてロジックが実行される

## Streamに適したテスト方法
- Blocは`Stream<Event>` なので `await untilCalled()` を用いてテストする

```test.dart
test(
  'should call the InputConverter to validate and convert the string to an unsigned integer',
  () async {
    // arrange
    when(mockInputConverter.stringToUnsignedInteger(any))
        .thenReturn(Right(tNumberParsed));
    // act
    bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
    await untilCalled(mockInputConverter.stringToUnsignedInteger(any));
    // assert
    verify(mockInputConverter.stringToUnsignedInteger(tNumberString));
  },
);
```

- `expectLater`
    - arrange / assert later / act の順となっている
- `emitsInOrder`
    - リスト順にemitされるのを検証するMatcher


```test.dart
test(
  'should emit [Error] when the input is invalid',
  () async {
    // arrange
    when(mockInputConverter.stringToUnsignedInteger(any))
        .thenReturn(Left(InvalidInputFailure()));
    // assert later
    final expected = [
      // flutter_bloc 最新versionではInitialStateがないため削除
      // Empty(),
      Error(message: INVALID_INPUT_FAILURE_MESSAGE),
    ];
    // 注: チュートリアルでは動かないので修正
    // bloc.state -> bloc
    // 動かない例: expectLater(bloc.state, emitsInOrder(expected));
    expectLater(bloc, emitsInOrder(expected));
    // act
    bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
  },
);
```

```implementation.dart
@override
Stream<NumberTriviaState> mapEventToState(
  NumberTriviaEvent event,
) async* {
  if (event is GetTriviaForConcreteNumber) {
    final inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    yield* inputEither.fold(
      (failure) async* {
        yield Error(message: INVALID_INPUT_FAILURE_MESSAGE);
      },
      // Although the "success case" doesn't interest us with the current test,
      // we still have to handle it somehow. 
      (integer) => throw UnimplementedError(),
    );
  }
}
```
## mapEventToState
- Eventに応じたStateを返すメソッド
    - yield の再帰形である yield*を返す
- UseCase（InputConverter）から`Either<Failure, int>`を受けとる
    - `fold`で分岐させることでRightとLeftの両方を処理する
        - これがRepository以降で扱ったEitherの終着点となっている

### 再帰的にStateを返す例

- `Loading, Loaded`のStateを返す例

```test.dart
test(
  'should emit [Loading, Loaded] when data is gotten successfully',
  () async {
    // arrange
    setUpMockInputConverterSuccess();
    when(mockGetConcreteNumberTrivia(any))
        .thenAnswer((_) async => Right(tNumberTrivia));
    // assert later
    final expected = [
      Loading(),
      Loaded(trivia: tNumberTrivia),
    ];
    expectLater(bloc.state, emitsInOrder(expected));
    // act
    bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
  },
);
```

```implementation.dart
@override
Stream<NumberTriviaState> mapEventToState(
  NumberTriviaEvent event,
) async* {
  if (event is GetTriviaForConcreteNumber) {
    final inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    yield* inputEither.fold(
      (failure) async* {
        yield Error(message: INVALID_INPUT_FAILURE_MESSAGE);
      },
      (integer) async* {
        yield Loading();
        final failureOrTrivia = await getConcreteNumberTrivia(
          Params(number: integer),
        );
        yield failureOrTrivia.fold(
          (failure) => throw UnimplementedError(),
          (trivia) => Loaded(trivia: trivia),
        );
      },
    );
  }
}
```


# 学んだこと
- Streamのテスト手法
    - `await untilCalled()`
    - `expectLater`
    - `emitsInOrder`
- Eitherの`fold()`メソッドを使用することで、Eitherから抜ける
- FailureのサブクラスはBLoC内ですべて処理する
    - ErrorメッセージをViewで表示するのがゴールとなる

---

# アドベントカレンダー完了
- 12/1に思いついてFlutterのアドベントカレンダーをはじめました。
- 私自身はプログラミング歴は浅く、どちらかというとディレクターやPM、開発人事などをやっていました
- ただプログラミングをやろうと決めて、Flutterをはじめました。
- アウトプットすると良いという話を聞いてアドベントカレンダーをやってみたら、自身に何度も説明するような感じで理解が深まったと思います。
- ただ、思いついて始めたのでネタがなかったです。途中でチュートリアルに頼りました。
    - それでも毎日続けるのが大事と思った次第です。一日でも途切れるとそれっきりになってしまいそうだったので、続けられる手段としてチュートリアル実況みたいにしました。

## これから
- チュートリアルはまだあと数回あるので、Qiita記事にします。
- その後も色々とQiitaを書くつもりです。
- ただ、毎日執筆はしないと思います。
    - 少しずつ質重視にシフトできたらいいなと思います。
- 以上です。ありがとうございました☆