アジャイルテスティング問答

先日、「人類よ!これがアジャイルテスティングだ!QAテックリードが語るアジャイルQAの実践とは何か? - connpass」というイベントに登壇させていただきました。
インタビュー形式だったので講演資料などは特に残ってないのですが、内容の記録のため公開に差し障りのない問答についてまとめたいと思います。念の為、一般的な定義というより、あくまで自分なりの考えになります。

Q. アジャイル開発におけるQAエンジニアの役割と責任は?

QAエンジニアの定義に幅があるので難しい問いですが、今回はプロダクトの品質を保証する役割の人を、QAエンジニアという前提で話します。

まずアジャイルウォーターフォールなどプロセスを問わない役割として、QAエンジニアは、様々な品質を保証する手段を直接担当したり、支援したりして、総体として品質を保証する仕組みを構築する役割だと考えています。
直接担当の役割としてはQAテスト、ドキュメントレビューなど、支援担当としては開発者テスト支援、プロセス運用支援、スプリントレビュー支援などが該当します。

次にアジャイル特有のQAエンジニアに求められる役割として、次の3点が大きくあると思います。

  • 1つめは、ユーザ観点での妥当な継続的フィードバックの実現です。
    動くプロダクトを作ってフィードバックを得て、より妥当なプロダクトを実現するサイクルを継続的な回せるのがアジャイルの強みです。それを支えていく役割です。
    具体的には、ユーザテストなどユーザからのフィードバックの機会を確保する、POと連携する、QAエンジニア自身がビジネスやプロダクトに詳しくなるといったアプローチで、ユーザ観点で妥当なフィードバックを継続的にチームに返せるようにするのが重要になります。
  • 2つめは、迅速なフィードバックの実現です。
    アジャイルの短期のイテレーションや高頻度な変更に対応するため、品質保証のスピードをアジャイルに合わせる必要があります。その迅速化を実現する役割です。
    その手段としては、自動化を始めとした品質保証の高速化技術を実践する、探索的アプローチで人間の能力主体でアジリティを確保する、といったものがあります。
  • 3つめは、Whole Team(チーム全体、Oneチーム化)として品質を保証する仕組みの実現です。
    ウォーターフォール的に、最終関門として最後に詳細なテストを最後にやるアプローチでは、変更に弱く、アジリティが低すぎて、アジャイルを阻害します。
    その解決として、開発者テスト、QAテスト、自動テスト、レビューなど、様々な品質保証の手段を連携させて、Whole Teamで品質保証していく仕組みづくりが、重要な役割だと考えます。

Q. アジャイルテスティングとは何か?

一般的には、大きく2つの定義があります。

  • 1つ目はジャネットグレゴリーの実践アジャイルテストなどが提唱する、テストそのものがアジャイルの原則に則っているアプローチ。
  • 2つ目は、ISTQBなどが提唱する、アジャイル開発をうまく支えるためのテストのアプローチが、アジャイルテストであると言う定義。TDDや継続的テスティングなどがこれに該当します。

今回は後者の定義で話を進めます。

基本的な方向性は、前述のQAエンジニアの役割を、テストで推進する形になります。様々ある中での一部ですが、具体的には次のような特徴を備えます

  • ユーザ観点の継続的なフィードバックをテストで実現します。
    ユーザテストを導入する、イテレーション内でシステムテストまで一通り行って、テストレベル横断のフィードバックを実現する、ビジネスやユーザに精通してそれらの観点に基づいたテストを実施する、といった特徴があります。
  • 変化を受け入れます。例えば柔軟に変更に対応するため、テストは自動化しますし、テストの保守性やテスタビリティの向上で、変更コストを削減していくアプローチが取られます。

注意として、アジャイルテストはプロセスや方法論と言うより、テストエンジニアのマインドセットやチーム文化、本質的な原則論です。
何かしらの手順通りにやればOKというものではなく、例えば開発者やPOとうまくコミュニケーションをとって、的確なテストを見出そう、みたいな姿勢の話になります。

Q. アジャイルテスティングをどうはじめればよいのか?

繰り返しですが、アジャイルテストはマインドセットやチーム文化、原則論の話になります。そのためその導入は人作り、チーム作りが主なアプローチになります。

その前提の話ですが、最初の方向性として次があると思います。

  • 妥当なフィードバックの実現を目指す。ユーザやPOと連携したり、テストチーム自身もプロダクトやビジネスについて精通したりして、ユーザ観点で妥当なテストを行えるようにチームを鍛えるのが最初の方向性だと思います。
  • アジャイルに合わせたアジリティの実現を目指す。最初は、手動テストから自動テストへの注力の転換、スクリプトテストから探索的テストへのアプローチの転換が取り組みやすいと思います。そこでは自動テストでリグレッションテストを実施しつつ、探索的テストで追加変更分をテストするスタイルを目指します。

上記の本質的な実現には、プロセスや手順の強化ではなくて、チームと人の強化に注力する必要があります。

Q. アジャイルテスト実現のためにテストのアジリティを高めるために何が必要か?

一言でいうと、人と開発技術とテスト設計の3つを鍛えていく必要があると思います。

人の強化

スピード重視のテストでは、探索的テストのアプローチが重要になります。一般的にも、アジャイルテストは探索的アプローチを取り入れたユーザストーリテストが定番の手段になります。
もちろんスクリプトテストが不可欠であるのは変わりないですが、伝統的なスクリプトテストは遅く、変化への妨げになります。スクリプトテスト、探索的テスト両方を組み合わせつつ、後者の比重を高めていきます。
この探索的なアプローチで有効なテストを行えるようにするためには、テストエンジニアを育てて、その能力を発揮させる必要があります。すなわちテストのアジリティを上げたいなら、テストエンジニアを精鋭化する必要があります。

開発技術の強化

アジリティのあるテストには様々な開発スキルが必要です。具体的には次が求められます:

  • テスト自動化。テストの高速化に有効ですし、リグレッションテストとしてアジャイルの変更への対応を実現する基礎になります。テスト自動化の実現には様々な開発スキルが求められます。
  • テスタビリティの向上。テスタビリティ技術の向上で、少ない手間でより有効なテストができるようになります。これには開発対象の設計・実装についての開発スキルが必要です。
  • テストの保守性の向上。長く継続的にテストのアジリティを確保するために重要です。例えばテストコードも適度にリファクタリングして、適度に保守性を組み込むといった話になります。これもテストコードの設計や実装といった開発スキルが求められる分野です。

テスト設計力の強化

アジリティあるテストを実現するためには、少ない手間で、十分なテストを実施する必要があります。
そこでは本質的な仕様やリスクを読み取って、ピンポイントでそれに対応する的確なテストを用意する必要があります。そのためには、チームのテスト分析やテスト設計のスキルを高めておく必要があります。

Q. テスタビリティの必要性とは?

テスタビリティの確保は、手動テスト、自動テスト、あるいはユニットストからEnd to Endテストまで問わず、アジャイルテストでとても重要です。

テスタビリティの確保は、アジャイルテストの要のテスト自動化のやりやすさにつながります。例えばテスト自動化を不能にする要因の影響範囲を小さくして、テスト自動化を実現できる範囲を広げたりするといったアプローチが可能になります。
またテスタビリティ確保は、変化に強いテストを実現するためにも必要です。例えばテストの変更性を高めて、アジャイルで継続的に変更する状況下で、テストが頻繁に壊れる問題(Fragile Test)を軽減する、といったことが可能になります。

テスタビリティの詳細や、具体的な実現アプローチについては以下を参照ください。

テスタビリティ(試験性)を確保するための設計方針 - 千里霧中

アジャイルテストで大規模テストに対応するにはどうするのか?

次の3方向の対応が有効だと考えます。

  • 1点目は、当たり前だと思いますが、チームの基礎的なテスト力を高めて、テストのスピードや妥当性を上げていくことが重要です。
  • 2点目は、テストアーキテクチャの組み立ての工夫が重要になります。プロジェクトを通して、大規模なテストアーキテクチャを育てていき、それに沿ってイテレーションごとのアジャイルテストを導いていくアプローチが重要です。日々の継続的なテストを、乱雑に積み上げるのではなく、全体整合が取りやすいように蓄積するアプローチです。
  • 3点目は、フロントローディングです。大規模テストの負荷・リスク対応・不具合検出を、日々のイテレーション内のテストで継続的に前倒し対応して、大規模なシステムテストの負荷を分散するアプローチが重要になります。

アジャイルテスティングのイベントに登壇

Ultimate Agilist Tokyoの10年越しの縁で藤原大さんから声をかけていただき、会社にも許可をもらって以下のアジャイルテスティングのイベントに登壇することになりました。

人類よ!これがアジャイルテスティングだ!QAテックリードが語るアジャイルQAの実践とは何か? - connpass

現在、アジャイル開発プロジェクトでテストのテックリードとしてテスト活動に従事しているほか、ソフトウェア開発でのTPSの推進を手掛けています。そのあたりのお話ができればと考えています。テーマに興味のある方がいたら参加いただけると幸いです。

世の中のアジャイルテスティングの定義と原則

アジャイルテスティングの定義や原則には、いくつかのバリエーションが存在します(大きく「アジャイル原則に適合したソフトウェアテスティング」or「アジャイルに適合したソフトウェアテスティング」の2つに分けられます)。今回は情報の整理のため、有力な文献をいくつかピックアップしました。

実践アジャイルテスト

実践アジャイルテスト テスターとアジャイルチームのための実践ガイド

アジャイルテスティングの原典の一つのように扱われる本書では、「アジャイルテスティングの定義とは」のように定義を簡潔に説明している記述はありません。
ただ参考にできる記述として、「アジャイルテスターの定義」を次のように解説しています。

アジャイルテスターとは、変化に対応し、技術担当の人や業務担当の人たちと共同作業でき、テストのコンセプトを理解して要求を文書化し開発をリードできる、プロフェッショナルなテスターです。アジャイルテスターの特徴は、高い技術力を持ち、メンバーと共同作業を心得てテストの自動化を行うことです。アジャイルテスターはまた経験豊富な探索的テスターでもあります。顧客が何をしたいかを常に気にかけており、顧客のソフトウェア要求を深く理解することができます。

基本的に、アジャイルの原則に沿ったテスティングを、アジャイルテスティングと定義しています。
またアジャイルテスティングの原則として、次を説明しています。

  • 継続的にフィードバックする
  • 顧客へ価値を提供する
  • 対面でのコミュニケーションを可能にする
  • 勇気を持つ
  • シンプルを心がける
  • 継続的な改善を実践する
  • 変化に対応する
  • 自分を律する
  • 人に焦点を当てる
  • 楽しむ

実践アジャイルテストの解説で特徴的なのは、アジャイルテスティングの実践においては、開発手法がアジャイルでなくてもよいと言及していることです。例えば「アジャイルテストとは、単にアジャイルプロジェクトにおけるテストではありません。探索的テストなどいくつかのテストは、プロジェクトがアジャイルでもそうでなくても、本質的にアジャイルです」といった解説がなされています。

More Agile Testing

More Agile Testing: Learning Journeys for the Whole Team

前述した実践アジャイルテストの補足および発展的内容を解説した本です。こちらもアジャイルテスティングの原典の1つとみなされる場合があります。
本書では「アジャイルテストの定義・原則とは」のように定義や原則を簡潔に解説している記述はありません。ただ著者が同じであることから、前述の実践アジャイルテストの原則を踏襲した解説となっています。

Agile Testing Condensed

Agile Testing Condensed… Yuya Kazama 著 et al. [PDF/iPad/Kindle]

この書籍は、前述の実践アジャイルテストとMore Agile Testingの内容の要点を解説した本です。執筆時点で日本語にてアジャイルテストを学ぼうとする場合で、第一に推薦すべき本となっています。
この本では、何度も聞かれるから、ということで、「アジャイルテストの定義」として次の解説を追加しています。

始まりからデリバリーまで、そしてそれ以降も継続的に実施される協調的なテストの実践により、お客様への価値の頻繁な提供をサポートします。テスト活動は、高速なフィードバックループを用いて理解を検証しながら、プロダクトの品質を築くことに重点を置いています。このプラクティスは、品質に対するチーム全体の責任という考え方を強化し、サポートします。

アジャイルテスティングの原則の解説も行っていますが、こちらは前述の実践アジャイルテストと同じ内容となっています。

Testing Manifesto

The Testing Manifesto | Growing Agile

Growing Agileでは、守るべきテスティングの原則として、Testing Manifestoという項目を次のように定義しています。この原則名にはアジャイルという言葉が含まれていませんが、「アジャイルをうまく実践するための知見を解説する」という文脈の中で記述されているものであり、アジャイルテスティングの原則の一つとして見なすことができます。

私たちは下記を大切にします:

  • 最後にテストするよりもずっとテストし続ける
  • バグの発見よりもバグの防止
  • 機能性をチェックするよりもチームが理解している価値をテストする
  • システムを破壊するよりも最高のシステムを構築する
  • テスターの責任よりも品質に対するチームの責任

※翻訳は前述のAgile Testing Condensed Japanese Editionから引用。

なおAgile Testing Condensedでも、アジャイルテスティングの原則の一つとしてこのマニフェストを引用しています。

JSTQB アジャイルテスト担当者

http://jstqb.jp/dl/JSTQB-SyllabusFoundation-AgileExt_Version2014.J01.pdf

ISTQB/JSTQBではアジャイルテスト担当者のシラバスを作成しており、そこでアジャイルテスティングに関する用語や概念を解説しています。ISTQB/JSTQBシラバスは世界共通の用語や概念として参照されることが多いため、今回簡単に触れます。

JSTQBアジャイルテスト担当者シラバスでは、「アジャイルテスティングの定義・原則とは」のような簡潔な解説を行っていません。
ただ、「アジャイルテスティング」=「アジャイルプロジェクトに適したテスティング」のような定義で一貫しています。
また記述が分散しているため引用は割愛しますが、開発活動のと関わり合い、テストに関する成果物、テストレベル、テスト成果物の構成管理など、いくつかのトピックごとに従来型テストと対比する形で、アジャイルテスティングの原則を解説しています。そこでも「アジャイルテスティングの原則」=「アジャイルプロジェクトに適したテストの原則」のような解説で一貫しています。「アジャイルテスティングは開発がアジャイルであることが前提」という立ち位置です。

なお近しい解説として、アジャイルテスト担当者の原則を、「持つべきスキル」として次のように解説しています。

チームメンバおよびステークホルダに対して、建設的かつソリューション指向で向き合う
プロダクトについて、批判的で、品質指向の、懐疑的な思考を発揮する
ステークホルダから情報を積極的に入手する(ドキュメント化された仕様に全面的に頼ることはしない)
テスト結果、テスト進捗およびプロダクト品質を正確に評価して報告する
テスト可能なユーザストーリー、特に受け入れ基準について定義するために、顧客の代表者やステークホルダと共に効果的に働くチーム内で協調し、プログラマおよび他のチームメンバとペアになって作業する
テストケースの変更、追加、または改善を含めて、変更に迅速に対応する
テスト担当者側での作業を計画し、準備する

Modern Testing Principles

Modern Testing – Not that modern, and not that much about testing

「Modern Testing Principles」はテスティングが重視すべき原則として定義されたもので、次の7項目で構成されます。アジャイルに限定されない原則ですが、アジャイルテスティングと方向性が一致しているものとして、アジャイルテスティングでも守るべき原則として解説されているのを度々見るため紹介します。

  1. Our priority is improving the business.
  2. We accelerate the team, and use models like Lean Thinking and the Theory of Constraints to help identify, prioritize and mitigate bottlenecks from the system.
  3. We are a force for continuous improvement, helping the team adapt and optimize in order to succeed, rather than providing a safety net to catch failures.
  4. We care deeply about the quality culture of our team, and we coach, lead, and nurture the team towards a more mature quality culture.
  5. We believe that the customer is the only one capable to judge and evaluate the quality of our product.
  6. We use data extensively to deeply understand customer usage and then close the gaps between product hypotheses and business impact.
  7. We expand testing abilities and knowhow across the team; understanding that this may reduce (or eliminate) the need for a dedicated testing specialist.

テスタビリティ(試験性)の拡張を実現する実装

前エントリ「テスタビリティ(試験性)を確保するための設計方針 - 千里霧中」の補足として、「テスタビリティ(試験性、Testability)を拡張可能にする」の実装について解説します。

拡張を実現する手段の一つに接合部(Seam)があります。今回は実装例として、接合部の一種である、メソッドインジェクションを使ってテスタビリティを拡張可能にする例を示します。

対象コード

次のtarget_method()をテストする場合について考えます。
target_method()は、device_controller_run()を呼び出して、外部デバイスを制御するメソッドです。

def device_controller_run(status):
    ...副作用を持つコード...

def target_method():
    ...
    device_controller_run(DeviceStatus.RUNNING)
    ...

テスタビリティを拡張可能にする

このtarget_method()をテスタビリティ拡張可能にします。その実現手段に、今回は次のようにメソッドインジェクションを導入します。

class DeviceController:
    def run(self, status):
    ...副作用を持つコード...

def target_method(device_controller):
    ...
    device_controller.run(DeviceStatus.RUNNING)
    ...

メソッドインジェクションは、対象の依存要素を、メソッド呼び出し時に注入するイディオムです。上記では、メソッドの引数としてDeviceControllerオブジェクトを渡し、それ経由で外部デバイス制御メソッドを呼び出すように変更しています。

メソッドインジェクションのような接合部を設けると、接合部を起点にして、後付けでテスタビリティを拡張できるようになります。

テスタビリティを拡張してユニットテストを実現する

ここで、target_method()に対するユニットテストを記述します。今回のユニットテストでは、target_method()が、適切な引数で外部デバイス制御メソッドを呼び出していることを確認します。

最初のtarget_method()の実装例では、このユニットテストを実装できるかは不明です。しかし、前述のようにメソッドインジェクションの導入でテスタビリティを拡張可能にしておくと、次のようなアプローチでテスタビリティを拡張して、ユニットテストを実現できるようになります。

class DeviceController:
    def run(self, status):
    ...副作用を持つコード...

class DeviceControllerWithTestability(DeviceController):
    def __init__(self):
        self._status = DeviceStatus.STOP

    def run(self, status):
        self._status = status

    def status_is_running(self):
        return self._status == DeviceStatus.RUNNING

def target_method(device_controller):
    '''テスト対象'''
    ...
    device_controller.run(DeviceStatus.RUNNING)
    ...
    
def test_target_method():
    '''ユニットテスト'''
    device_controller = DeviceControllerWithTestability()
    target_method(device_controller)
    assert device_controller.status_is_running()

上記では、テスト対象target_method()が依存するDeviceControllerを、DeviceControllerWithTestabilityに置換し、必要なテスタビリティを拡張しています。
テスタビリティの拡張ポイントは、DeviceControllerの副作用を解消する点、(やや過剰ですが)外部デバイス制御メソッドの引数をチェックするAssertionMethodを提供する点となります。

参考文献

接合部を含む、テスタビリティを拡張可能にするイディオムやパターンの解説としては、以下の書籍が充実しています。

レガシーコード改善ガイド | マイケル・C・フェザーズ, 平澤章, 越智典子, 稲葉信之, 田村友彦, 小堀真義, ウルシステムズ株式会社, ウルシステムズ株式会社 | コンピュータ・IT | Kindleストア | Amazon

テスタビリティ(試験性)を確保するための設計方針

テスタビリティ(試験性、テスト容易性)は「どれだけ容易にテストできるか」「どれだけテストを実現できるか」の度合いを示す品質特性です。
実践ソフトウェア・エンジニアリングの解説から引用すると、テスタビリティは次の特性から構成されます。

  • 実行円滑性(Operability)
    • テストの実行しやすさ。例えば、テスト実施をブロックする要因が少ないか。
  • 観測容易性(Observability)
    • テスト対象の観測のしやすさ。
  • 制御容易性(Controllability)
    • テスト対象の操作のしやすさ。
  • 分解容易性(Decomposability)
    • テスト対象の分離や分割のしやすさ。例えばテストのためにテスト対象を切り出しやすいか。
  • 単純性(Simplicity)
    • テスト対象の単純性。例えば機能がシンプルで必要なテストが少ないか。
  • 安定性(Stability)
    • テストに影響を与える変更の少なさ。
  • 理解容易性(Understandability)
    • テスト対象の理解しやすさ。

※この他にも、文献によってはプロジェクトの制約度合い(e.g.テストに十分なコストを確保しているか)、テストエンジニアのスキルといったテスト対象の外の特性もテスタビリティに含めているものがあります。

今回は、このテスタビリティを確保するための代表的な設計方針を解説したいと思います。

前提:様々なテストそれぞれにとってのテスタビリティがある

本題に入る前の前提の話です。テスタビリティは、あらゆるソフトウェアテストに関わる品質特性です。手動テスト、自動テスト、あるいはGUIを通したテスト、APIを通したテスト、コードレベルのテストなど種類を問わず、各々のテストにとって、それぞれのテスタビリティがあります。

例えば観測容易性を例をとります。テスタビリティの実装は、テストの種類ごとに次のように変化します。

  • 手動のシステムテストでの優れた観測容易性の実装例
    • 必要なエラーや実行情報を得るためのUIが提供されている
    • テストの前提条件(構成管理情報)を確認するためのUIが提供されている
  • APIテストでの優れた観測容易性の実装例
    • 必要な内部情報を得るためのAPIが揃っている
  • ユニットテストでの優れた観測用意性の実装例
    • 間接的な出力がなく、テストコードから必要な出力を直接簡単に参照できる

以降で解説する設計方針についても、具体的な実装は対象のテストの種類によって異なります。

テスタビリティを確保するための設計方針

テスタビリティを低下させる要因の影響範囲を小さくし、分離・置換できるようにする

テスタビリティを低下させる要因として、以下のような存在があります。

テスタビリティ確保の点では、こうしたテストの支障となるコンポーネントへの依存箇所を最小化すると、テスト可能な範囲が広がります。例えば以下のような対策が有効です。

  • 実行円滑性に劣るコンポーネント群は、Facadeパターンで簡易化したインターフェースを通して制御可能にする。
  • 安定性に劣るコンポーネントは、コンポーネントをなるべく局所化した上で、ラッピングして他との依存性を削減する。例えばUIデザインが頻繁に変更されるなら、UI層を最小化・分離するなど。

さらに、上記のテスタビリティを低下させる要素は、後述する接合部を設けて分離・置換できるようにすると、テスタビリティ確保の助けになります。例えば観測容易性に劣るコンポーネントを、観測手段を埋め込んだTest Doubleに置換できるようにするといった工夫です。

結合度を低く、凝集度を高く

一般的な設計の方針として、コンポーネント間の結合度を低くし、凝集度を高くすることが推奨されていますが、この方針はテスタビリティの改善にも繋がります。
結合度を低くするように設計・実装すると、テスタビリティのうち分解容易性が向上します。凝集度を高くするように設計・実装すると、テスタビリティのうちの単純性や理解容易性が高まります。

特に結合度を低く保つ設計については、設計の要所に接合部(Seam)を設けることが重要です。接合部は、特定のコンポーネントを切り離して他に置換できるようにするための仕組みです。依存性の逆転(Dependency Inversion)、Link Seam(ビルドを分割しリンク時に切り替えられるようにする)などがあります。接合部があると、テスト対象を切り離したり、テストの障害となるコンポーネントをTest Doubleに置換したりすることができるようになり、テストにとって十分な結合性の低さを確保できます。

テストにとって十分な観測点、制御点を設ける

観測点(Observation Point)はテストの出力を得るための手段です。その充実度が観測容易性に直結します。制御点(Control Point)はテスト対象を操作するための手段です。その充実度が制御容易性に直結します。

テスタビリティの確保においては、プロジェクトのそれぞれのテストが求める観測容易性、制御容易性の要求を識別して、それを満たす観測点・制御点を設けることが重要になります。
例えばテストに必要な観測・制御ができるようにテスト用インターフェースを設ける、テストで観測すべき情報がすべて盛り込まれるようにログ設計する、テストにとって必要な情報が得られるようにエラーなど情報通知UIを設ける、といった工夫が有効になります。

テスタビリティを拡張可能にする

後からテスタビリティを拡張可能にすると、テスタビリティを確保できなくても、テストを実施するタイミングで必要なテスタビリティを後から確保できるようになります。
また、開発成果物が将来レガシーコード化する備えの点でも、各所にテスタビリティが注入可能なポイントを実装しておくのが有効です。その備えを行っておくと、将来レガシーコード化しても、テスタビリティを注入して必要なリグレッションテストを構築し、そのテスト使って脱レガシーコードのためのリファクタリングを行うといった対策が打てるようになります。

テスタビリティを拡張可能にする手段としては、例えば前述した接合部があります。接合部を設けると、テストが依存するコンポーネントを、観測・制御手段付きに改造したコンポーネントに置換できるようになります。

テストでとり得る条件を制限する

テストの入出力のパラメータや値が少ないと、テスタビリティのうちの単純性が向上するほか、観測や制御が容易になります。
このパラメータや値の削減には、次のような工夫が有効になります。

  • 変数やメソッドなどのスコープを狭め、グローバルな依存関係を削減する
  • 冗長な引数や戻り値をなくす
  • 内部状態をシンプルに保つ
  • 副作用の発生可能性をなくす
  • 型でとり得る値を制限する

また、値の組み合わせなど、テストでとり得る条件を削減すると、同じく単純性が向上します。この実現のためには、前述のパラメータや値の削減のほか、次のような工夫が有効になります。

  • 制御フローをシンプルに保つ。例えばエラーチェックを冒頭で行ってネストを浅くするなど。これによりデシジョンテーブルの圧縮やCFD法の適用などが可能になる。
  • ロックなどの排他処理や割り込み保護を適切に行い、タイミングの組み合わせを削減する
  • 禁則の組み合わせを、実装上の工夫で実現不能にする
  • DRYを推進する

品質特性のバランスを取る

テスタビリティは、一部の品質特性とトレードオフの関係を持ちます。代表例は、性能効率性やセキュリティです。
セキュリティを例に取ると、例えばテスト用に設けたAPIセキュリティホールになるといった問題が、トレードオフの関係例です。

テスタビリティの確保にあたっては、テスタビリティとトレードオフになる品質を特定して、両者を両立する設計を見つけ出す必要があります。
例えばテスタビリティとセキュリティのトレードオフの場合ですと、次のようなアプローチが設計として有効になります。

  • トラストバウンダリを設け、その内部にテストインターフェースを設ける
  • テスト用とデプロイ用を安全に切り替えられるよう、コードやビルドシステムを工夫する

設計やコードをリーダブルに保つ

理解しやすい命名をするといった、リーダブルにするための設計やコードの原則は、単純性や理解容易性を改善し、テスタビリティを底上げします。

Flutter Integration Testingでは、testパッケージではなくSDKのintegration_testパッケージを使う

 前のエントリ「テストコードのデザインパターン:Robotパターン」を書いた際に情報提供いただいたネタです。
 現在、FlutterのIntegration Testでは、flutter_driverパッケージとtestパッケージの組み合わせではなく、SDKのintegration_testパッケージでテストコードを記述することが公式で推奨されています。

An introduction to integration testing - Flutter

Note: The integration_test package is now the recommended way to write integration 
tests. See the Integration testing page for details.

 SDKのintegration_testパッケージは、先月stable版がリリースされたFlutter2から利用可能になっているものです。

 integration_testパッケージは旧来のパッケージと比べて、次のようなメリットがあります。

  • Firebase Test Labでの実行をサポート
  • flutter_test APIを使って、Widget Testと同等の構文でIntegration Testを記述できる
  • 旧来のFlutter Driveコマンドに対応していて、物理デバイスでもエミュレータでもIntegration Testを実行できる

 今回はこのintegration_testパッケージを使った簡単なテストコードを示します。

pubspec.yaml

dev_dependencies:
  ...
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter
  ...

テストコード

 旧来のIntegration Testのテストコードは次のようになります

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  FlutterDriver driver;

  setUpAll(() async {
    driver = await FlutterDriver.connect();
  });

  tearDownAll(() async {
    await driver.close();
  });

  group('E2E Test for Login', () {
    test('authenticate a user', () async {
      await driver.tap(find.byValueKey('usernameTextField'));
      await driver.enterText('hoge hoge');

      await driver.tap(find.byValueKey('passwordTextField'));
      await driver.enterText('sample password');

      await driver.tap(find.byValueKey('login'));

      final indexMenuFinder = find.text('Execute');
      await driver.waitFor(indexMenuFinder);
      expect(await driver.getText(indexMenuFinder), 'Execute');
    });
  });
}

 integration_testパッケージを使用すると、上記のテストコードは次のように記述できます。Widget Testと同じようなスタイルになります。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
      await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');

      await tester.tap(find.byKey(Key('login')));

      await tester.pumpAndSettle();

      expect(find.text('Execute'), findsOneWidget);
    });
  });
}

テストコードのデザインパターン:Robotパターン

 アプリケーションなどのテストコードのデザインパターンの一つに、Robotパターン(Testing Robotパターン、Robot Testingパターン)という汎用的なパターンがあります。

Presentation: Testing Robots - Jake Wharton
Robot Pattern Testing for XCUITest | by Rob Whitaker | Capital One Tech | Medium

 Robotパターンは、テスト対象のセグメンテーション(例えば画面)ごとにRobotオブジェクトを用意し、テストコード上にテストのWhat(テストしたい振る舞いや仕様)を、RobotオブジェクトにテストのHow(テスト対象の操作や期待値比較の詳細な実装や、テスト対象への直接の依存部分)を実装するものです。これによりテストコードの可読性や再利用性を向上させることを目的とします。

 このパターンは、テストコード上にWhatを、PageオブジェクトにHowを実装する、一般的なPage Objectパターンと目的やアプローチが類似しています(ただRobotパターンは、Robotオブジェクト内にAssertion機能を実装するのを許容するという違いがあります)。

 今回はFlutterアプリを対象に、このRobotパターンの実装例を示します。

テスト対象のサンプル

 HOME画面に次のようなログイン画面を表示する、単純なログイン機能を有したFlutterアプリを対象にします。今回は、これに対し、ログインして、Executeという内部機能実行ボタンが表示されることを確認するIntegration Testを実装します。

f:id:goyoki:20210422003746p:plain

Robotパターンを使用しないテストコード

 Robotパターンを使用せずにIntegration Testを実装した例を次に示します。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
      await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');

      await tester.tap(find.byKey(Key('login')));

      await tester.pumpAndSettle();

      expect(find.text('Execute'), findsOneWidget);
    });
  });
}

 テストコードにはusernameTextFieldといったキー名など、テスト対象の詳細な操作がハードコーディングされています。

Robotパターンを使用したテストコード

 次にRobotパターンを使用してIntegration Testを実装した例を次に示します。

 まず画面ごとにRobotオブジェクトを用意します。Robotオブジェクトでは、テスト対象の操作や期待値比較についての詳細な実装(How)を、わかりやすい名前(What)のメソッドにラッピングしていきます。

// login_robot.dart(ログイン画面のRobotクラス)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class LoginRobot {
  const LoginRobot(this.tester);

  final WidgetTester tester;

  Future<void> enterUserName(String username) async {
    await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
  }

  Future<void> enterPassword(String password) async {
    await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');
  }

  Future<void> tapLoginButton() async {
    await tester.tap(find.byKey(Key('login')));
    await tester.pumpAndSettle();
  }
}
// index_robot.dart(ログイン後画面のRobotクラス)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class IndexRobot {
  const IndexRobot(this.tester);
  final WidgetTester tester;

  Future<void> checkUserIsOnTheIndexScreen() async {
    expect(find.text('Execute'), findsOneWidget);
  }
}

 次にテストコードです。Robotオブジェクトを経由してテスト対象を操作することにより、テストコードは次のように実装できます。usernameTextFieldといった内部のキーへの直接の依存がなくなり、テストしたい振る舞いや仕様が端的に表現されたコードになります。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'robots/login_robot.dart';
import 'robots/index_robot.dart';

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      final loginRobot = LoginRobot(tester);
      final indexRobot = IndexRobot(tester);
      app.main();
      await tester.pumpAndSettle();

      await loginRobot.enterUserName('hoge hoge');
      await loginRobot.enterPassword('sample password');
      await loginRobot.tapLoginButton();
      await indexRobot.checkUserIsOnTheIndexScreen();
    });
  });
}