1
1

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 TDD Clean Architecture Course その11, 12 - Blocの実装

Last updated at Posted at 2020-12-24

  • 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を書くつもりです。
  • ただ、毎日執筆はしないと思います。
    • 少しずつ質重視にシフトできたらいいなと思います。
  • 以上です。ありがとうございました☆
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?