Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterテスト講座 - テスト設計できるようになろう

cch-robo
October 23, 2021

Flutterテスト講座 - テスト設計できるようになろう

DevFest Kyoto 2021 (Flutterテスト講座) セッション
「テスト設計できるようになろう」発表スライドです。

アプリは作れるけれどテストは...という、テスト初心者を対象に、
テストの原則『自動テストは、「外部から見た振る舞いの検証」』
...という視点に立ち、

テストしやすいアプリ設計と、
テスト時のみ依存内容を テストダブルに差し替える アプローチを使い、
「要件や仕様のみで、テスト仕様がゼロの段階」から、
「要件や仕様を満たすテストコードを作成」する考え方を示します。

スライドに合わせて、サンプルリポジトリも御参照下さい。
cch-robo/flutter_extreme_test_sample
https://github.com/cch-robo/flutter_extreme_test_sample

DevFest Kyoto 2021 - Flutter テスト講座 (2021/10/23)
https://gdgkyoto.connpass.com/event/226491/

cch-robo

October 23, 2021
Tweet

Other Decks in Technology

Transcript

  1. 4 Flutter公式サイトには、 Unit test, Widget test, Integration test について、 Test

    API の使い方や flutter test コマンド実行などの ドキュメント① が掲載されています。 ですが、どうアプリやテストを設計すれば良いのか、 …テストしやすいアプリや、テスト仕様の作り方については、 開発者に任されています。 ① Testing Flutter apps ⇒ https://flutter.dev/docs/testing  Integration testing ⇒ https://flutter.dev/docs/testing/integration-tests  Cookbook Testing -  Integration, Unit, Widget ⇒ https://flutter.dev/docs/cookbook#testing  Flutter アプリのテスト方法 ⇒ https://codelabs.developers.google.com/codelabs/flutter-app-testing/
  2. 留意事項 7 このセッションでは、最低限の機能テストができることを目指します • 具体的な要件や機能仕様に基づく、 最低限の自動テストが設計&作成できるようになることに限定します。 ◦ 非機能要件など、セキュリティやフォールトトレランスのような 別途専門知識に基づいた対応が必要となる要件は対象としていません。 ◦

    契約納品物としてのテストや設計図書の作成は対象としていません。 このためホワイトボックスなどのテスト方法や、境界値テストなどの テスト技法、およびテストケース仕様書やテスト手順書などの テスト設計図書の作成についての説明は行いません。 ◦ 自動テストの設計についての講座ですので、Test API の使い方は最小限です。
  3. 8 ソフトウェアテスト見積りガイドブック https://www.ipa.go.jp/files/000005132.pdf ユーザが自ら実践! 最新事例で学ぶ要件定義の勘どころ 第一部 全体概要 P.70 https://www.ipa.go.jp/files/000085749.pdf 非機能要件や

    契約納品物としての テストでは、多岐に渡ったテスト方法や テスト技法、テスト設計図書が必要です。 その詳細は、専門書籍で確認願います。 専門書籍例
  4. 完璧なテストは 高コスト 初心者には敷居が高い 前2ページで紹介しましたように、 バグを事前検出し、要件を満足するか、デグレ発生がないかの完全に近い保証は、 多岐に渡ったテスト方法やテスト技法、テスト設計図書が求められます。 ホワイトボックステストの実施には、完全な仕様理解とコード解析が必要です。 10 • ブラックボックステスト

    要求仕様の入力・出力値通りに、コンポーネントが振る舞うかをチェックする。 テストコード自体は簡易だが、同値クラステスト、境界値テスト、状態遷移テスト、 異常値/無効値テスト…などテストパターン組み合わせを網羅する必要がある。 • ホワイトボックステスト コードの内部構造を基に、要求仕様の関係経路コードを全て網羅したチェックを行う。 経路の全分岐条件を確認し、条件を満足させるには、テストコードが複雑になる。
  5. 気後れしないよう、完璧を目指さず、 要求動作が確認できる設計を目指します。 「要望は、漸次変化するのでコードの内部構造も変化していく。」として、 ホワイトボックステストを除外し、ブラックボックステストを簡略化した、 「自動テストによる、外部から確認できる入出力値による要求動作の検証」  ⇒「外部から見た振る舞いの検証」のみに抑え、完璧を目指しません。 11 ここでの テストの 目標

    ソフトウェアで最も大切なのは「振る舞い」であり、振る舞いこそがユーザの求めるものである。 期待される振る舞いを私たちが追加すればユーザーは喜ぶが、ユーザの求める振る舞いを変更、 あるいは削除してしまえば、バグの作り込みとなり、私たちへの信頼は失われてしまう。 出展元【書籍】リファクタリング (Martin Fowler) レガシーコード改善ガイド ソフトウェアの変更 から引用
  6. Flutter標準 3カテゴリの自動テスト 14 • Unit test 単一関数の入出力や 単一クラスのメソッドと状態管理を検証する 単体テスト です。

    クラスや関数に直接介在できるので、テスト時の入力に テストダブル と呼ばれる テスト用のオブジェクトが作成でき、入力と出力や状態が正しいことを検証します。 • Widget test ウィジェットへの UIイベントなどによる UI変化を検証します。 ウィジェットツリーを前提とした ウィジェット用の単体テスト です。 • Integration test アプリを実機やシミュレータにインストールして起動し、 ボタンクリックでの画面表示変化など、 UI へのイベントと UI 変化を検証します。 結合テスト と呼ばれ、コンポーネントが連携して正しく動作することを検証します。
  7. テストダブル 15 テスト前提やテスト検証を満たすよう作成された、 実際のオブジェクトと置き換える 影武者オブジェクト。 • Fake テストの前提を満たすよう作成されたもの。 • Dummy

    テストに影響を与えないが、テストに必要なもの。 • Stub テスト対象と直接関係しないが、テストで間接的に必要なもの。 • Spy テスト中に操作された記録をとり検証可能にする 特殊なもの。 • Mock テスト中に参照される内容を登録しておき フローを操作するもの。 テストダブル https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E3%83%80%E3%83%96%E3%83%AB xUnit Test PatternsのTest Doubleパターン(Mock、Stub、Fake、Dummy等の定義) https://goyoki.hatenablog.com/entry/20120301/1330608789
  8. アーキテクチャの基本 18 アーキテクチャは、アプリを継続的に開発可能にする ための構築ルールです。 Flutterでは、関心事と制御を分離するための MVVM+α①が一般的でしょう。 そして、基本的に変わらないものと、漸次変化してくものへの意識②も必要です。 • 依存関係のレイヤ構造のように、基本的に変わらないもの。 •

    操作性改善のための UI 刷新や、サービス機能の追加や変更に削除など、 常に改良(漸次変化)していくもの。 ①α ⇒ Repository や UseCase など。 ②MVVM の構造は変化しなくても、UseCaseなどの中身は漸次変化する。 【参考】  Flutter を MVVM で実装する  https://wasabeef.medium.com/flutter-%E3%82%92-mvvm-%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B-861c5dbcc565
  9. 依存性の分離 20 たとえば、架空の商品取引を扱うため、以下の 型 があるとします。 ①.輸入国から輸出国への小麦の商品取引を扱う CommodityTrading ②.米1ドルを基準に当日の各国為替レートを返す ExchangeRate ③.小麦1㌧を基準に当日の各国相場価格を返す

    MarketPrice ①は、② と ③ のオブジェクトを内部で利用して、  輸入国から輸出国への取引差額の利益率 profit を算出するとして、 そのメソッドを以下とします。  CommodityTrading#trading(Country import, Country export): profit  テストしやすい アーキテクチャの基本
  10. 依存性の分離           21 ここで、実は「商社ごとに 毎日の相場価格の 取引方法が異なる」上に、 取引できる商社は、追加されたり削除されたりするという内部事情があれば、 MarketPrice をインターフェース⇒抽象にして、 A商社、B商社ごとに AMarketPrice、BMarketPrice

    …実装クラス⇒具象を作成し、 全体から最適な、輸入国に利用する商社、輸出国に利用する商社を選択して、 ①のCommodityTrading に MarketPrice⇒抽象として渡す内部実装にすれば、 どの商社が選ばれても同列の同じものとして扱えるので、  CommodityTrading#trading(Country import, Country export): profit  …は、B商社が削除されて、C商社が新たに追加されても変わらず使えます。 テストしやすい アーキテクチャの基本 最適な輸出入国が渡されるかをテスト検証する場合、 具象だと対象となる商社を受け取るテストコードが必要ですが、 商社が削除されると当該テストコードの修正も必要になるでしょう。
  11. 依存性の分離はテストダブルにも有効 22 依存性の分離は、本質だけの取扱実装でオブジェクトを生成できるので、 テストダブルも作りやすくなります。  前ページの CommodityTrading が、  コンストラクタ引数で渡された MarketPrice(抽象) リスト

    から、  最適な輸入商社と輸出商社を選択するロジック があるとして、 その機能をテストしたいのなら、  商社ごとに異なる「毎日の相場価格の 取引方法」を実装することなく、  「小麦1㌧の当日の各国相場価格」の フェイクを実装したテストダブルが作れます。  予め最適な、輸入国用の商社と 輸出国用の商社を想定したリスト渡すことで、  単体テストでの検証もおこなえるでしょう。 テストしやすい アーキテクチャの基本 あたりまえすぎて、ごめんなさい。 󰢜
  12. テストダブルについての注意 23 依存性の分離 により、本質だけの取扱実装でオブジェクトを生成できるので、 テストダブル が作りやすいのですが、モック の生成については注意が必要です。 モックは、何らかのロジックによる実際の判定結果を詰め込むことで、 参照先の処理フローに影響を与えるものです。 つまりモックにする対象の

    単体テストによる振る舞い保証 がされていないと、 モックに詰め込む(設定する)値や状態が、適切か否かも保証できませんし、 悪影響を及ぼしかねません。 モックに 理想値を詰め込んで、テストが通ったとしても、 そのグリーンパス (テスト合格) は意味がないかもしれないからです。 モック利用の前に、単体テストを実施して 実際の結果を記録してください。 テストしやすい アーキテクチャの基本
  13. 状態と状態区分を参照可能にする 24 自動テストによる、外部から見たふるまいの検証 を行うためには、 振る舞いを表す 状態プロパティ が外部から参照可能である必要があります。 状態が {初期値⇒データフェッチ中⇒取得成功/エラー} のように遷移するなら、

    状態区分を Dart 列挙型 (enum) で定義して公開し、その状態型としてください。  理想論ですが、状態プロパティには、個別の状態型を定義しましょう。  例えば、釣った魚の数で等級 {1: 3匹以上、2: 2匹以下、3: 1匹, 4: 0匹 }をあらわす場合、  int の魚数と等級の列挙型と対応を一括管理する 釣果等級クラスを作ります。  単純に魚数 int、等級 intとするよりも、意図や情報が伝わり易くなります。 テストしやすい アーキテクチャの基本
  14. テストしやすさの視点は、他にもあります 25 テストのしやすさの視点は、 テスト対象のサブクラス Test-Specific Subclass を作るなど、 注視点ごとに様々な手法 (詳細はブログを参照) が利用できます。

    ですがメリット・デメリットがあることと、 万能解がないことに留意ください。 t-wadaさんのブログより 現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ https://t-wada.hatenablog.jp/entry/design-for-testability おすすめの ブログ記事です。
  15.      継続的に開発可能にする基本原則 26 SOLID オブジェクト指向設計原則 S:The Single Responsibility Principle (単一責任の原則) O:The

    Open Closed Principle (オープン・クローズドの原則) L:The Liskov Substitution Principle (リスコフの置換原則) I:The Interface Segregation Principle (インターフェース分離の原則) D:The Dependency Inversion Principle (依存性逆転の原則) 関連する設計原則 【補足】 アーキテクチャの基本 SOLID (オブジェクト指向設計原則) https://ja.wikipedia.org/wiki/SOLID Inversion of Control(制御の反転) 制御の反転 (Inversion of Control) https://ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E3%81%AE%E5%8F%8D%E8%BB%A Law of Demeter(デメテルの法則) デメテルの法則 (Law of Demeter) https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%A1%E3%83%86%E3%83%AB%E3%81%AE%E6%B3%95%E5%89%87
  16.      継続的に開発可能にする基本原則 27 SOLID・制御の反転・デメテルの法則に従って、 コンポーネントに対して、責務の最小化、抽象による依存都合の最小化と依存関係の分離、上下階 層関係を持ちながら、制御の流れの往来化、上下階層の越境の禁止 …を厳守させた、 変化による影響範囲を最小にする、アーキテクチャ設計を行います。  単一責任の原則は、クラスを単機能にし、多重責務による 依存関係爆発を排除 します。

     制御の反転は、コールバックハンドラのように 被呼出側が呼出側を逆に制御 します。  デメテルの法則は、直接関与する相手のみ取り扱い、 その先への越境を許しません。  上下階層関係構造では、上位は下位をバインドします。(逆は原則としてありません)  これにより、コンポーネントが上意下達のフローをとるツリーを作ります。 【補足】 アーキテクチャの基本
  17. 上意下達のフローを確保する 28 上下階層関係構造では、上位は下位をバインドします。(逆は原則としてありません) これにより、上意下達のフローをとる コンポーネントツリー を作ります。 • これは上位が下位を制御し、下位よりも長命であることを明示します。 メモリリークを避けるため、上位は自分と運命をともにしない下位に対し、 ライフサイクルを把握し、nullable

    にして初期化と破棄を徹底してください。 • コールバックを使って下位⇒上位の制御を行う場合は、 直接上位の具象を扱わず、下位からの制御操作のみを提供する 仲介抽象インターフェース を下位のパッケージに用意してください。 上位は 仲介抽象を実装し、下位は 仲介抽象型 のオブジェクトをバインドします。 つまり下位パッケージに、上位パッケージの import が発生しないようにします。 【補足】 アーキテクチャの基本
  18. 上意下達のフローを確保する 29 上意下達のフローをとるコンポーネントツリーでは、 コンポーネントの仲介抽象を利用しない制御の反転を禁止します。 【補足】 アーキテクチャの基本 OK BAD 【凡例】 直接制御

    制御反転 制御反転の徹底は理想論です。 採用不採用はコストを見て御判断ください。 󰢜 BAD コールバックを 明示しないので 回転させても、 上位下達に なってしまう。 どっちが 上位
  19. 上意下達のフローを確保する 30 上意下達のフローをとるコンポーネントツリーでは、 コンポーネントの越境利用(越権利用)を禁止します。 【補足】 アーキテクチャの基本 OK OK BAD ViewModelが

    UseCase を越えて Repositoryを越境利用 している。 【凡例】 直接制御 制御反転 越境利用禁止は理想論です。 採用不採用はコストを見て御判断ください。 󰢜
  20. 上意下達のフローを確保する 31 上意下達のフローをとるコンポーネントツリーでは、 コンポーネントの貸与所有(参照貸与)を制御と区別します。 【補足】 アーキテクチャの基本 【凡例】 直接制御 制御反転 貸与所有

    • Provider や Riverpod などは、 コンポーネントツリーの上流で、 アプリ全体共有のオブジェクト を保持し、 context を介して下流から参照させて 貸与物を直接制御 できるようにします。 オブジェクトの保持 / 所有が、 直接制御 のためか 貸与所有のためかは、 制御メソッドの利用有無で判断するしか ありません。 UseCaseは、 MyAppが貸与所有する Repository を参照貸与を 介して直接制御している  貸与所有や参照貸与は、独自概念です。 󰢜 ① ② ③ ④
  21. コンポーネントツリー例 32 カウンターアプリ に ViewModel を利用した場合の 上意下達のフローをとる コンポーネントツリー は、 右図のようになります。

    コンポーネントの 直接制御①と 反転制御②は、 全て直接アクセスをとり、間を挟んで飛び越えず、 上位から下位へ上意下達の制御フローになっています。 ① 下位をバインドしてメッセージの送信や状態を参照する。 ② リスナに上位をバインドさせコールバックで同期制御する。 【補足】 アーキテクチャの基本 Riverpod の内部挙動は 想定 です。 実際と異なってる可能性が高い旨、 お許しください。
  22. コンテキスト境界を分ける 33 実際のアプリは、前ページのコンポーネントツリーの ViewModel 下位にウィジェットツリーを越えた、 UseCase と Repository などのビジネスロジックや 外部

    Web APIを使う Infrastracture があるでしょう。  右図では、WidgetTreeや Business Logic などの  レイヤ境界をつけていますが、これらの境界は、  純粋なロジックから排除できない 外部都合により   区別しています。 境界をまたぐデータは、 下位に仲介抽象インターフェースを設け、各境界の具象から 仲介抽象に変換して、ドメインモデル貧血症 を抑止します。 【補足】 アーキテクチャの基本 境界ごとのデータ変換は理想論です。 採用不採用はコストを見て御判断ください。 󰢜
  23. 要求仕様とは 35 要求仕様は、 〜は、〜したら、〜して、〜なら、〜する…というような、 起点と各時点での状態が指定された、要求のフローです。 アプリは、システム設計や 詳細設計(内部設計) により、 各コンポーネントが連携して、要求仕様を満たす実装になっていますから、 境界付きの上意下達をとるコンポーネントツリーに対し、

    要求のフローに対応する、起点から状態を指定どおりに更新する フローになります。 つまり要求仕様に対応する、 コンポーネントツリーでの上意下達のフローが具体的に見えないと、 (検証方法も見えてこないので) テスト設計ができないことになります。
  24. テストコードを起こす 36 要求仕様に対応する、 コンポーネントツリーでの上意下達のフローが具体的に見えているのであれば、 フロー中で参照される依存コンポーネントに、前提を満たす初期値を与えて、 要求仕様の起点に対応するイベント(画面表示やボタンタップ)でフローを走らせ、 要求仕様の状態に対応するコンポーネントの状態プロパティについて、 初期値から指定の状態値になるかを検証するコードを書けば良いことになります。  たとえば コンポーネントツリー例

    のなら、FAB(タッチ) ⇒ ViewModel(カウント更新) ⇒  NotificationProvider ⇒ CounterView(最新カウント表示) のフローになるので、  ViewModel を初期状態にして、FAB にタッチイベントを発生させれば、自動的に  ViewModel count状態プロパティが更新(+1) されるのを確認するテストコードになります。 count状態プロパティが初期値の 0 から +1 されて、1 になることを確認します。
  25.   37 コンポーネントツリー例 カウントアップのフロー FAB タッチイベント①  ↓ ViewModel カウント更新 ViewModel

    表示更新(notify)②  ↓ NotificationProvider ③④  ↓ CounterView 最新カウント表示⑤ ① ② ③ ④ ⑤ Widget Tapイベントなので、 Widget testか Integration testで 動作させる必要がある。 Widget testや Integration testでは、 状態プロパティを確認 できないので表示更新の結 果を検証する。 https://github.com/cch-robo/flutter_extreme_test_sample/blob/ main/test/widget_test.dart (Widget test コードサンプル参照) 後述の Extreme テストなら、FAB タッチイベント① だけで、 ViewModel カウント更新 (count プロパティ更新) を検証可能
  26. Widget test で状態プロパティを参照する 境界付きの上意下達のフローをとるコンポーネントツリーを アプリ全体の依存性ツリー ⇒ ルートからのウィジェットツリーと一致させるには、 • Widget test

    のテスト対象ウィジェットツリーをアプリ・ルートからにするか、 • アプリをインストールして動作させる Integration test を使う必要があります。 ですが Widget test や Integration test には、 任意コンポーネントの内部状態を参照する方法が提供されていません。 そこでアプリコードに、 「テスト中にコンポーネントツリー内の任意オブジェクトを  外部参照可能なオブジェクトに差し替えられる」 独自Factory を導入することで、 テスト中での テストダブルの注入や コンポーネント状態の確認ができるようにした、 Extreme / 極端なテストサンプルを作成してみました。  38
  27. エクストリームテスト アプリコードに、 「テスト中にコンポーネントツリー内の任意オブジェクトを  外部参照可能なオブジェクトに差し替えられる」独自Factory を導入したアプリと、  テスト中のオブジェクト差替で、コンポーネント状態を検証するサンプルです。 39 cch-robo / flutter_extreme_test_sample

    プロジェクトリポジトリ https://github.com/cch-robo/flutter_extreme_test_sample flutter_extreme_test_sample/lib/main.dart     独自Factory導入済みカウンタアプリ https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/lib/main.dart flutter_extreme_test_sample/integration_test/app_extreme_test.dart Extreme Integration test https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/integration_test/app_extreme_test.dart flutter_extreme_test_sample/test/widget_extreme_test.dart Extreme Widget test https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/test/widget_extreme_test.dart  詳細はコードを  御確認ください。
  28. エクストリームテスト (2) 一覧画面で ユーザを選択(タップ) すると、詳細画面に遷移して、 選択されたユーザ情報を表示するアプリの 結合テストも用意してみました。  テストコードでは、疑似リポジトリ からユーザ一覧を取得 し、

     選択ユーザ情報を受領 して、詳細画面へ遷移 させる、ビジネスロジック を注視して、  アイテムタップを起点とした、上意下達フローによる状態の自動更新を検証しています。 40 flutter_extreme_test_sample/lib/main_2.dart   独自Factory導入済み画面遷移アプリ https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/lib/main_2.dart flutter_extreme_test_sample/integration_test/app_extreme_test_2.dart Extreme Integration test https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/integration_test/app_extreme_test.dart flutter_extreme_test_sample/test/widget_extreme_test_2.dart Extreme Widget test https://github.com/cch-robo/flutter_extreme_test_sample/blob/main/test/widget_extreme_test_2.dart ツリーの構造は、 スライド P.31 を 参考にしてください。
  29. キーポイント(極言) • アプリ設計は、 境界付きの上意下達フローをとるコンポーネントツリーの作成です。 • 要求仕様は、{ 〜したら、〜して、〜する }という、 起点から指定した状態への更新を要求する 要求フロー

    です。 • アプリ開発は、 要求フローを満たすよう、対応する状態を管理するコンポーネントと、 起点から指定した状態への更新を行わせる 上意下達のフローをとるコンポーネントツリーの作成です。 • コンポーネント開発は、上意下達のフローをとるため、 (管理する状態の)単一責任の原則と、依存の分離と、制御の反転の明示と、 制御の越境禁止を厳守します。 42
  30. キーポイント(極言) 43 • 自動テストは、最低限 外部から見たふるまいの検証を行うこと。 (外部参照可能な ふるまいの達成を表す状態の 入出力値の変化検証) • テスト仕様は、

    要求フローを満たすよう実装されたコンポーネントツリーから、 起点と上意下達のフローを具体的にすること。 (具体的にできなければ、テストを作成することはできない) • テスト設計は、 要求フローを満たすコンポーネントツリーの起点と上意下達のフローから、 要求された振る舞いを表すコンポーネント状態の初期値と結果値と、 依存するコンポーネントの前提状態を満たすテストダブルの状態値設定の 具体化です。