flutter + reduxでのMiddleware(非同期処理)のテスト

## 概要

flutter + reduxの構成でのmiddleareの単体テストの書き方を紹介します。テスト用のDSLはどの言語で勉強するのも大変ですね。

※reducerのテストはdispatchしてstateをexpectするだけなのでここでは説明しません

## Prerequisite

pubspec.ymlは以下の通り。APIコール部分はモックするためにMockitoを使用します。

```pubspec.yml
dependencies:
  flutter_redux: ^0.6.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^4.1.1
```

### テスト対象のmiddleware

あるActionが呼ばれたらAPIをコール、その前後でLoadingに関するStartとCompleteのActionがdispatchされるmiddlewareのテストを書くこととします。
以下のようなmiddlewareを想定してください。

```root_middleware.dart
Middleware<AppState> rootMiddleware(MyClient myClient) =>
  return TypedMiddleware<AppState, FetchStartAction>(_fetch(myClient)
}

void Function(
  Store<AppState> store,
  FetchStartAction action,
  NextDispatcher next,
) _fetch(MyClient myClient) {
  return (store, action, next) {
    next(LoadingAction());
    myClient.fetch().then((res) {
      next(FetchSucceedAction(res: res));
    })
    .whenComplete(() {
      next(LoadCompleteAction());
    });
  };
}
```

### テストコードのセットアップ

具体的なテストコードを書く前の準備として必要なクラスのモック化、およびStoreの宣言などを行います。

```root_middleware_test.dart

// APIコールを行うclientをモック化
class MockClient extends Mock implements MyClient {}

class Watcher extends Mock implements MiddlewareClass<AppState> {}

void main() {
  final mockClient = MockClient();
  final watcher = Watcher();

  // storeの初期化
  final store = Store<AppState>(
    appReducer,
    initialState: AppState.init(),
    middleware: [rootMiddleware(
      myClient: mockClient,
    )]..add(watcher),
  );

  // APIクライアントのfetch()をモックする
  when(mockClient.fetch())
      .thenAnswer((_) {
    return Future.value(mockedResult);
  });

  // 各テストの前にFetchStartActionを呼ぶ
  setUp(() {
    store.dispatch(FetchStartAction());
  });

  // 各テスト後にmockMiddlewareを初期化する
  tearDown(() {
    reset(watcher);
  });

  // 全テスト終了後にmockClientを初期化する
  tearDownAll(() {
    reset(mockClient);
  });

```

middlewareに対して、既に`rootMiddleware`が代入されているのに`..add(watcher)`していますが、
これは実際のmiddlewareの挙動を確認するためのインスタンスです。
`Mock`を`extends`したクラスのインスタンスは呼ばれたメソッドを全て記録しています。`verify()`を使ってどのメソッドが呼ばれたかを後から検証できます。

## MiddlewareからActionがdispatchされたことを確認する

先にコードを見せますが、以下のようになります。

```dart
group('before call api', () {
  test('dispatch LoadingAction', () async {
    verify<void>(
      watcher.call(
        store,
        predicate<LoadingAction>((action) => action is LoadingAction),
        any,
      ),
    );
  });
});
```

この記事で1番大事なのはこの部分↓ですが、分かりにくいので分解して解説します。

```dart
verify<void>(
  watcher.call(
    store,
    predicate<LoadingAction>((action) => action is LoadingAction),
    any,
  ),
);
```

まず、上のコードを単純化すると以下のようになります。

```dart
verify(watcher.call(...));
```

`verify(mockedObj.hoge())`は`mockedObj.hoge()`が呼ばれたか検証します。つまり、ここでは`watcher.call()`が呼ばれたか検証しています。

```dart
watcher.call(
  store,
  predicate<LoadingAction>((action) => action is LoadingAction),
  any,
)
```

次に、`watcher`はdispatchされたActionを全て記録しているため、このactionの型をpredicateで制限して検証しています。
これらを組み合わせると、「`LoadingAction`が正しくdispatchされたか」を検証できます。

```dart
verify<void>(
  watcher.call(
    store,
    predicate<LoadingAction>((action) => action is LoadingAction),
    any,
  ),
);
```

※ただし、watcherが記録しているのはそのテスト中に実行されたAction全てであるため「`verify()`したActionがテスト対象のmiddleewareからdispatchされたものであるかは検証できない」という点に注意してください。

## MiddlewareからdispatchされたActionのプロパティを確認する

基本的に要領は同じです。先にコードを見せます。

```dart
test("action's result is mockedResult", () async {
      await untilCalled<void>(
        watcher.call(
          store,
          predicate<LoadCompleteAction>(
              (action) => action is LoadCompleteAction),
          any,
        ),
      );
      verify<void>(
        watcher.call(
          store,
          predicate<FetchSucceedAction>(
            (action) {
              return action is FetchSucceedAction &&
                     action.result, mockedResult;
            },
          ),
          any,
        ),
      );
    });
```

先ほどと違うのは以下の部分です。

```dart
await untilCalled<void>(
  watcher.call(
    store,
    predicate<LoadCompleteAction>(
      (action) => action is LoadCompleteAction),
    any,
  ),
);
```

これは`untilCalled`で`LoadCompleteAction`が記録されるのを待っています。`LoadCompleteAction`は`.whenComplete`内でdispatchされるように書いているので、これによってAPIコールの結果を待っています。

参考：https://pub.dev/documentation/mockito/latest/mockito/untilCalled.html

```dart
verify<void>(
  watcher.call(
    store,
    predicate<FetchSucceedAction>(
      (action) {
        return action is FetchSucceedAction &&
               action.result, mockedResult;
      }
    ),
    any,
  ),
);
```
後は↑ですが、actionに渡した値ががAPIclientのモックの値と同じかを検証しています。


## まとめ

middlewareのテスト、やはり

> `verify()`したActionがテスト対象のmiddleewareからdispatchされたものであるかは検証できない

のがちょっと厳しい感はありますね…
dartのreduxでmiddlewareをテストするコードがググってもあんまりサンプルが出てこないので、もっと良さそうな書き方をご存じの方はご教示くださいm(__)m

### 雑記

Actionがdispatchされたかどうかよりもstateがどう変わったかのレベルでテストしたほうが良いかもしれないと書いてからおもいました

でもそれだと1回のActionで複数回変更されるLoadingのようなstateの変更をテストするのが難しいからmiddlewareをモックしてverify()していくしかないとやっぱりおもいました


## メモ：API1行解説

### redux

#### middleware.call()

https://pub.dev/documentation/redux/latest/redux/MiddlewareClass/call.html

dispatchされたactionをreducerが処理する前にmiddlewareが呼ばれた時の関数。
storeを初期化する際に代入した全てのmiddlewareに対して`call()`が呼ばれる。

例えば、公式のexampleにもある通り、以下のようにすれば`print()`がreducerの前に実行される。

```dart
class LoggingMiddleware extends MiddlewareClass<int> {
  call(Store<int> store, action, NextDispatcher next) {
    print('${new DateTime.now()}: $action');

    next(action);
  }
}

// Create your store with the loggingMiddleware
final store = new Store<int>(
  counterReducer,
  middleware: [new LoggingMiddleware()],
);
```

`TypedMiddleware`はこれを便利にしただけのクラスなのでこちらも当然`call()`が呼ばれる。ただしこちらは`<T>`に代入したActionの場合のみ。

https://pub.dev/documentation/redux/latest/redux/TypedMiddleware-class.html

### dart/test

#### predicate()

https://api.flutter.dev/flutter/package-matcher_matcher/predicate.html

テストしたい値を元に何かしらの計算をしてアサーションする時に使う関数(要はMatcherを返しているだけ)。
具体的には以下のようにして使う。

```dart
class Rect {
  Rect(this.length, this.width);
  final int length;
  final int width;

  int get area => length * width;
}

test('what is predicate', () {
  final hasCorrectArea = predicate<Rect>((rect) {
    return rect.area == rect.length * rect.width;
  });
  final myRect = Rect(10, 10);
  expect(myRect, hasCorrectArea);
});
```

#### setup(), tearDown()

各テストの前後に処理を挟む関数。setup()で各テストの前にdispatch()、後にmockMiddlewareの初期化を行ったりする。

### mockito

https://pub.dev/documentation/mockito/latest/mockito/mockito-library.html

#### verify()

https://pub.dev/documentation/mockito/latest/mockito/verify.html

以下のようにして、モックされたオブジェクトがそのメソッドを呼んだかを判定するメソッド。

```dart
cat.eatFood("chicken"); // catはmockをextentedしたクラスのオブジェクト
verify(cat.eatFood("chicken")); // passed
verify(cat.eatFood("fish"));    // failed
```

## 参考

- https://qiita.com/ko2ic/items/78c4a035a0aef9cfc78d#middleware%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88
- https://pub.dev/packages/test#asynchronous-tests