Go Fuzzingによるファジングテスト/ランダムテスト

Goは標準のテストフレームワークでファジングをサポートしています。今回はそのファジング機能について、テスト条件の網羅をどうするかを中心に使い方をメモします。

基本的なテストコードの書き方

Fuzzingを使ったテストコードの簡単な例を以下に示します。
テスト対象Targetを実行し、エラーが発生しないことを確認しています。Seed Corpus(ユーザ指定の入力のセット)は未指定です。

import (
	"testing"
)

func FuzzTarget(f *testing.F) {
	f.Fuzz(func(t *testing.T, i int8) {
		_, err1 := Target(i)
		if err1 != nil {
			return
		}
	})
}

大雑把に基本ルールのみに絞って説明すると、Go Testingのテストコードの記法や命名(*_test.goのファイル名にするなど)を踏襲した上で、以下のような記述を行います。

  • テストメソッドは「Fuzz」から始める
  • 実行時はgo tesetに「-fuzz」オプションを付与する
  • (*testing.F).Fuzzでテストメソッドをラップする

既に以下のような明快な解説が複数存在するため、詳細な説明は割愛します。

Go Fuzzing - The Go Programming Language
Tutorial: Getting started with fuzzing - The Go Programming Language
Go1.18から追加されたFuzzingとは | フューチャー技術ブログ

デフォルトでFuzzingが生成する入力値データ

試しに上記のコード(int8を入力)を対象に、Seed Corpus未指定のまま、2秒間Fuzzingを実行(1491回テスト実行)して得られた値の出現数を示します。

この試行を行った場合では、入力値の全パターンに対する網羅率は98%でした。割と広く網羅していますが、今回は一般的に品質リスクの高いと見なされる下限の「-128」の境界値を網羅できていませんでした。

ファジングツールはランダムデータに加えて、一般的に品質リスクの高いパターンを反映したファズデータを入力に用いることが多いです。
一方、Go Fuzzingは、テスト対象のASTを網羅するように探索するものの、ブラックボックス観点や外部の知見を使用しません。その点でいうと、Seed Corpus未指定では、Go FuzzingはHaskellのQuickCheckと同種の、ランダムテストのツールと見なしても良いと思います。

なおSeed Corpusを指定すると、Go Fuzzingはその入力セットとそれを変異させたデータを入力として生成するようになります。すなわち、ブラックボックス観点での品質リスクを使ってファジングテストとしての有効性をより高めるためには、テストコード実装者がテスト条件を分析して追加指定する必要があります。

Fuzzingで特定のテスト条件を生成させる

Fuzzingの入力値生成エンジンに特定のテスト条件を生成させるためには、そのテスト条件をデータファイルあるいは(*testing.F).Addの手段を使って、Seed Corpusとして指定します。
たとえばファジングでint8の境界値を網羅するように指定する場合、次のように記述します。

func FuzzTarget(f *testing.F) {
	condisions := []int8{127, -128}
	for _, cd := range condisions {
		f.Add(cd)
	}
	f.Fuzz(func(t *testing.T, i int8) {
		_, err1 := Target(i)
		if err1 != nil {
			return
		}
	})
}

上記のようにSeed Corpusでテスト条件を指定すると、そのテスト条件と、その一部を変異させたテスト条件を網羅するようにFuzzingが動作します。サンプルコードの場合、127、-128とその周辺値をテスト条件に用いるようになります。

テスト失敗時のテスト条件に基づいたテスト条件の生成

ファジングでテストが失敗した場合、実行ディレクトリの「testdata/fuzz/テスト名/自動生成したファイル名」に失敗時のテスト条件をSeed Corpusとして記録します。
例えば以下のテスト対象を対象にファジングを実行し、前述のファジングのテストコードでエラーを発生させてテスト失敗させてみます。

func IntTarget(i1 int, i2 int) (int, error) {
	if i2 == 1 {
		return 1, errors.New("")
	}
	return 0, nil
}

すると前述のディレクトリパスに以下のSeed Corpusファイルを生成します。

go test fuzz v1
int(-65)
int(1)

※-65はテスト失敗時のi1の入力値

以降、同じファジングテストを実行する際は、Fuzzingは記録されたテスト条件i1=-65、i2=1を網羅・頻出させるように動作します。

コードカバレッジに基づいたテスト条件の生成

Fuzzingはテスト対象のコードカバレッジが変化するテスト条件をinteresting pointとして抽出し、そのデータを用いてよりコードカバレッジを網羅するように入力値を生成させます。
例えば以下のようなテスト対象・テストコードでファジングを実行します。

//テスト対象
func IntTarget(i1 int, i2 int) (int, error) {
	if i1 == 100 && i2 == -100 {
		fmt.Print("hogefuga")
	}
	if i1 == 200 {
		fmt.Print("piyo")
	}
	return 0, nil
}
//テストコード
func FuzzIntTarget(f *testing.F) {
	f.Fuzz(func(t *testing.T, i1 int, i2 int) {
		_, err1 := IntTarget(i1, i2)
		if err1 != nil {
			t.Error("error")
			return
		}
	})
}

するとFuzzingは、i1については100と200およびその周辺値、i2については-100およびその周辺値を頻出させるように入力値を生成します。

C++ コンセプト(concepts)の型制約のテスト

C++20から導入されたコンセプト(concepts)は、テンプレート型のパラメータの制約を実現する、型制約の言語機能です。
コンセプトを使うと、従来のSFINAEなどのテクニックと比べて、簡潔に型の制約を記述できるようになるほか、制約違反時のエラーメッセージを格段に読みやすくできます。
ただコンセプトによって、型の制約をより複雑に書けるようになる側面があります。そこで需要が増えるのが、型制約のテストです。複雑なコンセプトを書けばプログラミング中に確認を行いたくなりますし、また例えばライブラリなどの汎用的なAPIでは型制約は重要な品質保証の対象になります。

コンセプトのテストの記述

コンセプトはコンパイル時に評価され、それ単体では制約を満たせばbool型でtrueを、制約に違反すればfalseを返す挙動をとります。そのためコンセプトのテストは、コンパイル時評価手段のstatic_assertを使って簡単に実現できます。
サンプルコードを示します。

#include <concepts>

template <class T, class U>
concept Comparable = requires (T t, U u)
{
  {t == u} -> std::convertible_to<bool>;
};

class Dummy
{//Comparableの制約を満たさないダミー
};

int main()
{
  //コンセプトのテスト
  static_assert(Comparable<int, double>);
  static_assert(Comparable<int, bool>);
  static_assert(not Comparable<int, Dummy>);//等価比較不能
}

以下がテスト対象のコンセプト記述です。等価比較演算できることを制約で保証します。このコンセプトが適用されたテンプレートのパラメータが制約違反すると、コンパイル不能になります。

template <class T, class U>
concept Comparable = requires (T t, U u)
{
  {t == u} -> std::convertible_to<bool>;
};

以下がコンセプトのテストの記述になります。

  static_assert(Comparable<int, double>);
  static_assert(Comparable<int, bool>);
  static_assert(not Comparable<int, Dummy>);//等価比較不能

static_assertでコンパイル時テストとして処理します。コンセプトを満たすことを確認するテストはコンセプトをそのまま、コンセプトに違反することを確認するテストはnot演算子を付与して記述します。
今回のコンセプトはこのテストに合格するため、コンパイルに成功します。

コンセプトのテストに失敗する場合

仮に、Dummyで次のように等価比較をオーバーロードして、Comparableが成立するように改変します。

class Dummy
{
public:
	bool operator == (const bool obj) const
	{
		return true == obj;
	}
};

これを前述のコンパイル時テストで実行すると、コンパイルが次のエラーで失敗するようになります。

sample.cpp:22:17: error: static assertion failed
   22 |   static_assert(not Comparable<int, Dummy>);
      |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~

JaSST'22東北でテスト自動化について登壇

先日、JaSST’22東北にて、「テスト自動化の成功を支えるチームと仕組み」と題して招待講演を行う機会をいただきました。

内容は、テスト自動化を支える活動や、テスト自動化の基礎作りについて、長年の経験で得た経験や知見を整理したものになります。

docs.google.com

他の講演の内容と相乗効果もあり、今回はかなり好評だったようで安堵しています。
運営者・参加者の方々、貴重な機会をいただきありがとうございました。

テストカバレッジを具体的に表現するためのアプローチ

 構造やコードに対するテストカバレッジは、テスト対象のモデルが比較的明快であるため、表現が容易ですし、既に様々なカバレッジが世の中で使用されています。
 例えばコードの場合、次のようなカバレッジでテストカバレッジを表現できます。

 一方、システムテストなどでの、ブラックボックスの要求や仕様に対するカバレッジについては、テスト対象のモデルが複雑・不明瞭であることもあり、カバレッジの表現が適切でない場面をよく見ます。
 例えば、そういった場面では、テスト設計の網羅目標として、次のようなカバレッジのみでテストカバレッジを表現する事例をよく見ます。

  • 仕様項目(ユーザストーリ、ユースケース、USDMの仕様など)に対する網羅率
    • 仕様項目一つ一つに対し、対応するテストを用意できているか
  • テストベースに対する網羅率
    • テストベース(例えば要求仕様書)の一文一文に対し、テストを漏れなく用意できているか

 あるいは、上記より少しだけ踏み込んだレベルで、「画面遷移を網羅できているか」「選択肢を網羅できているか」といった、特に目立つ側面のみのカバレッジで表現している場合もあります。

 このレベルのテストカバレッジは、大規模開発でテスト設計を俯瞰把握したり、組織に最低限のテストを求めたりするような、ざっくりとした用途では有効です。

 しかし、こうしたカバレッジは、テストの目標やテスト設計の十分性基準を表現するものとしては不足しています。
 例えば「仕様項目(例:ユーザストーリ)に対する網羅率が100%である」というテストカバレッジを守らせても、ユーザストーリ→テストのトレーサビリティの関連づけができる程度のことしか保証できません。ユーザストーリに対して適切な網羅ができているかは触れられていない状態です。

カバレッジが不明瞭な場面でのテストカバレッジの表現アプローチ:表現可能になるまでテスト観点を具体化する

 ではテスト設計の目標・十分性基準を具体的に明文化したり、指定したりするためにどうすればいいかについてですが、広く有効なアプローチとして「カバレッジを適切に表現可能になるまで、テスト観点を分解する」というものがあります。

 具体例として、特定のアプリの画面のテストのカバレッジを表現したい場合、次のようにテスト観点を分解します。

  • 文言
    • 各言語の正確さ
    • 文字切れ
  • アクセシビリティ基準の準拠
  • アニメーション
    • 滑らかさ
  • 実行デバイス
    • OS
      • 種類
      • バージョン
    • 画面レイアウト
  • ・・・

 このようにテスト観点を具体化しておくと、「一通りの画面・画面遷移を網羅できていること」といったざっくりとしたカバレッジ表現ではなく、「指定のOSのバリエーションを網羅していること」「アクセシビリティ基準を満たしていること」といった具体的なカバレッジ表現ができるようになります。

 なおこのテスト観点の抽出は、マインドマップやテスト観点分析ツリーといった、ズームアウト・ズームインを表現できるモデリングで行うと、カバレッジ表現の点で都合が良いです。標準的なカバレッジ表現をしたいならばテスト観点の抽象度を上げ、具体的な対象のカバレッジ表現をしたいならば抽象度を下げ、といったアプローチが取れるようになります。

テストタイプの設計アプローチ(標準規格の品質モデルで分割してはいけない)

 特定のテストレベル(例えばシステムテスト)で大規模で複雑なテストを設計する場合、目的や十分性基準、テスト設計方針を具体的に考えるために、関心事の分離を実施する必要があります。
 その手段の一つとして、テストタイプの設計があります。そこでは巨大なテストの活動をテストタイプに分割することによって、テストタイプごとに関心事を分けて活動を進められるようにします。

まずいアプローチ:品質モデルの標準規格そのままに分割する

 このテストタイプの設計手段ですが、ISO25010といった標準的な品質モデルをそのままテストタイプの切り分け方に適用するアプローチをしばしば見ます。
 例えば、ISO25010の主特性「機能適合性」「性能効率性」「互換性」「信頼性」「セキュリティ」「移植性」をそのまま使って、テストを「機能適合性テスト」「性能効率性テスト」「互換性テスト」「信頼性テスト」「セキュリティテスト」「移植性テスト」に分ける、というアプローチです。

 ただこれは不適切なアプローチです。
 標準規格の品質モデルは、テスト活動のやりやすさを考慮して分けられたものではありません。さらにあくまで標準的なモデルであり、実際のあるべき品質モデルはプロジェクトや開発物によって大きく異なります。そのため標準規格の品質モデルでテスト活動を区分けしようとすると、以降のテスト活動にて、分析作業の重複が多発するなどといった支障が発生します。

有効なアプローチ:テスト対象に適した品質モデルと、テスト活動のやりやすさを観点に設計する

 ではどうするかについてですが、テストタイプの導出は、テスト対象に適した品質モデルの観点を分析するのがメインアプローチになります。
 それに加えて、以降のテスト活動が実施しやすいか」の観点を使ってテストタイプを具体化・調整していくのが有効です。この「以降のテスト活動が実施しやすいか」のより具体的な観点をいくつか以下に記載します。

「テスト分析・テスト設計がしやすいか」

 例えば、テスト分析を行えるサイズまで、テスト観点あるいはテスト条件をテストタイプとしてグルーピングするという観点です。大規模で複雑なテストは、テスト観点あるいはテスト条件も膨大かつ複雑で、そのままではテスト分析が困難です。それを扱えるサイズまでパッケージングできれば、以降のテスト分析も容易になります。

 また、テスト分析・テスト設計の担当の能力に合わせて、テストタイプを分割するというのも、この観点の主要なアプローチです。
 例えばセキュリティテストだけ特別なスキルを持つある専門チームでしかテスト設計できないならば、セキュリティテストとそれ以外のテストをテストタイプとして分割することで、プロジェクトが以降の作業を効率的に進められるようになります。

 さらに、テスト分析・テスト設計を行うタイミングに合わせて、テストタイプを分割するのも、この観点に基づいた設計アプローチです。
 例えばテスト分析・テスト設計に必要なインプット情報の入手時期が大きく異なるならば、早期に扱うテストタイプ、後期に扱うテストタイプと、入手時期ごとに分割することで、時期に応じてテスト設計アプローチを変える対応が可能になります。

 設計・実装のアプローチに合わせて、テストタイプを配置するという観点もあります。
 例えばユーザストーリをベースに開発しているならば、ユーザストーリテストと、それを補完するテストにテストタイプを分割することで、ユーザストーリ単位の開発アプローチを支えつつ、補完的な品質保証を実施するアプローチを容易に遂行できるようになります。

「テスト実施がしやすいか」

 例えば、必要なテスト環境・テスト機材に応じて、テストタイプを分割するアプローチです。
 特殊なテストラボでしか実施できないテストがあるならば、特殊なテストラボで実施するテストタイプと、それ以外のテストタイプを分割することで、テスト実施環境に合わせたテスト活動を円滑に遂行できるようになります。

 テスト実施のサイクルやタイミングに合わせて、テストタイプを分割するのもこの観点の一種です。例えば1週間のスプリントごとに実施するユーザストーリテストと、1か月程度のリリースサイクルごとに実施するリリーステストがあれば、そのサイクルを基準にテストタイプを分割すると、サイクルに応じたテスト活動方針の最適化が容易になります。

テストタイプ間の連携をどうするか

 テストタイプの設計で関心事の分離を推進した場合で課題となるのが、テストタイプ間の連携です。
 
 テスト活動は全体として品質保証を達成する必要があり、テストタイプの組み合わせに漏れがあると問題が発生します。また無駄なリソースやコストを軽減するために、テストタイプ間の重複のムダは、少ないほうが良い場合が多いです。さらに困難なテストの課題に対応する際は、それぞれのテストタイプの強みを活かすように、テストの重ねわせやテストの連携協力が求められます。

 そのため、テストタイプを分割すれば、あとはバラバラに活動すれば良いというわけではなく、分割後もテストタイプ間で相互連携をしながらテスト活動を推進する必要があります。

 この連携の管理方法ですが、有効なものにテストタイプの分割アプローチのモデリングがあります。例えば「GSNでテストタイプを分析する - 千里霧中」で紹介したGSNによってテストタイプ設計のアプリーチをモデル化しておくと、テストタイプそれぞれが全体の中でどのような責務を持っているか明確になります。またあるテストをどのテストタイプで実施すべきかの判断フロートしてもモデルを活用できます。

 また課題やリスクのコントロールとして、テストタイプをどのように連携させるか管理するアプローチも有効です。例えば「テストを導くためのテストアーキテクチャの組み立て方/cetam - Speaker Deck」で紹介しているリスクコントロールとしてテストタイプをどのように組み合わせるかを管理しておくと、テストタイプがどのテストタイプとどのように連携すべきかを明示できるようになります。

ソフトウェアテスト技術文書の英語読み書き・付け焼き刃勉強法

Calendar for ソフトウェアテストの小ネ | Advent Calendar 2021 - Qiita」の記事です。

今回は、ソフトウェアテストの英語技術文書のリーディング・ライティングを最低限できるようにするための、手っ取り早い付け焼き刃勉強法に触れます。
先の前置きとして、この方法は英語勉強法として十分ではありません(ドメインやサービス固有の文章、組織固有の文章は対応できません)。ただ、純粋なソフトウェアテスト技術についての文章ならば、片言レベルのリーディング・ライティングに手っ取り早く対応できるようになると思います。

手っ取り早いソフトウェアテスト技術文書の英語リーディング勉強法

ISTQBのシラバスJSTQBシラバスを使用します。

JSTQBのシラバス
ISTQBのシラバス

その中で、英語版と日本語版両方が発行されている区分を使用します。基礎的な用語・文書をカバーしたい場合は、シラバスのうち以下の3つを対象とするのが良いと思います。

  • Foundation Level
  • Advanced Level Test Analyst
  • Advanced Level Test Manager

また余裕があるならば、以下も対象にすると、扱える文章が広がります。

  • Foundation Level Agile Tester
  • Test Automation Engineer

勉強法ですが、英語のISTQBシラバスをスラスラ読めるようになるまで、日・英のシラバスを併読します。
わからない用語が出てきたら、逐次辞書などで調べるのは少なめに、日本語シラバスから意味を掴んで、勉強のスピードとテンポを維持します。
結構量がありますが、英語シラバスの文書は簡潔で、限られた用語や表現が繰り返し頻出する内容となっているため、進展するほど勉強スピードが高まります。量ほど大変ではありません。

この勉強法でISTQBシラバスを難なく読めるようになったら、ドメインやプロダクト・組織固有の文書を除く、純粋なソフトウェアテストについての技術文章は、おおよそ意味がつかめるようになっていると思います。

注意として、日本語シラバスには誤訳の可能性があるのに考慮が必要です。また改定で内容が結構変わるので、併読の際は日英シラバスでバージョンを一致させる必要があります(バージョンは日本語シラバスの改訂履歴で照合できます)。

手っ取り早いソフトウェアテスト技術文書の英語ライティング対応法

ライティングについては、勉強法ではなく、付け焼き刃対応法です。
こちらもISTQBのシラバスJSTQBシラバスを使います。

やり方ですが、まず前述のリーディング勉強法で、英語のシラバスをスラスラ読めるようになるまで勉強します。
すると、シラバスのどこに何が書かれているかが、ある程度イメージできるようになります。

その上で、ライティングでは次のような手順で対応します。

  1. ライティングしたい元の日本語に似た文章を、想起してJSTQBの日本語シラバスからピックアップします。
  2. 日本語シラバスの該当箇所に対応する英語文章を英語シラバスからピックアップします
  3. 英語シラバスの該当文章を参考に、英語文章をライティングします(コピペすると引用になり著作権等の制約が発生するため、あくまで参考にして英語文章を作ります)

例えばテスト目的についての文章を英訳したい→JSTQBのFLシラバスにテスト目的の解説があるのを思い出す→そこに対応するISTQBシラバスの文書を参考にする、といった流れで対応します。

この方法により、シラバスで書かれている文章限定ですが、ネイティブの文章を参考にライティングできるようになります。

欠点

  • ドメインやプロダクト固有の英語には対応できません。ソフトウェアテストの文書にとって、ドメインやプロダクト固有の文章は不可欠であり、内容の大部分を占めると思います。その部分は別で補う必要があります。
  • ISTQB/JSTQBシラバスは一人称・二人称を徹底的に廃した文章ですので、それに従ってライティングを行うと「仕様書みたいな文章」と言われます。

Tesseract OCRで文言描画の多言語対応テストを自動化する

Calendar for ソフトウェアテスト | Advent Calendar 2021 - Qiita」の記事です。

多言語をサポートするプロダクトの開発では、各言語ごとに表示文言が正しく描画されているかテストしたい場合があります。
今回は、その文言描画の多言語対応テストを、Tesseract OCRで自動化するアプローチについて解説します。

環境

テスト対象

今回はTwitterのUIのうち、英語、日本語、ドイツ語を対象にします。
次のような画像をキャプチャして、2列目の領域にそれぞれの言語の文言が適切に描画されているか、ビジュアルテストのアプローチで確認します。

eng.png
f:id:goyoki:20211215030022p:plain

jpn.png
f:id:goyoki:20211215030032p:plain

deu.png
f:id:goyoki:20211215030044p:plain

テスト

文言の内容の正確性(例えば翻訳の正確性)のテストは、実装中の文言データをチェックする方法が妥当な場合が多いです。
今回はそれ以外の、文言の描画の正確性(文字切れがないか、変な改行などないか、そもそも描画されているか)を自動でテストします。

1. 対象画面をキャプチャする

プロダクトの種類に応じた自動操作アプローチで、対象の画面を片っ端から静止画保存します。今回のように、WebサービスのUIを対象にして、pythonベースでテストを組む場合は、seleniumなどが妥当な実現手段になると思います。
今回は、その手段で、前述した英語・日本語・ドイツ語の設定画面キャプチャ画像(eng.png、jpn.png、deu.png)を、テスト対象画面として静止画保存します。

2. キャプチャ上の文言をOCRで抽出し、テキストベースで文言が正しいか確認する

次に、キャプチャ画像から文言を抽出して、テキストベースで文言の正確性を確認します。

テストオラクルとしては、コードが参照する文言データを利用できる場合が多いと思います。
サンプルレベルのかなり簡略化した例ですが、今回は、次ようなCSV形式で各国文言を列挙した文言ファイルがあるとします。テストではこれを読み込んで期待値に展開します。

stringdata.csv

"Your account","アカウント","Dein Account"
"Security and account access","セキュリティとアカウントアクセス","Sicherheit und Account-Zugriff"
"Privacy and safety","プライバシーと安全","Datenschutz und Sicherheit"
"Notifications","通知","Mitteilungen"
"Accessibility, display, and languages","アクセシビリティ、表示、言語","Barrierefreiheit, Anzeige und Sprachen"
"Additional resources","その他のリソース","Zusätzliche Ressourcen"

次にテストコードですが、一例として次のように実装できます。

test_display_all_words.py

import re
import csv
from PIL import Image
import pyocr
import pyocr.builders

def check_all_words(wordsfile, language, csv_column):
    '''指定された言語の表示文言が表示されているかチェック
    '''
    tool = pyocr.get_available_tools()[0]

    txt = tool.image_to_string(
        Image.open(language+'.png').crop((560, 100, 1200, 700)),#対象領域トリミング
        lang=language,
        builder=pyocr.builders.TextBuilder(tesseract_layout=6)
    )
    #言語特有の文字列処理
    if language == 'jpn':
        result = re.sub(r'([あ-んア-ン一-龥ー、])\s+((?=[あ-んア-ン一-龥ー、]))',r'\1\2', txt)
    else:
        result = txt

    with open(wordsfile) as f:
        reader = csv.reader(f)
        string_list_set = [row for row in reader]

    for string_list in string_list_set:
        if not string_list[csv_column] in result:
            print(f'failed: [{string_list[csv_column]}] is not found') #テスト失敗時の失敗箇所の提示のため-sオプション推奨
            return False
    return True

def test_all_words():
    '''各言語ごとの表示文言のチェック
    '''
    lang_list = [['eng', 0], ['jpn', 1], ['deu', 2]] #[lang_name, column_index of csv]

    for lang in lang_list:
        assert check_all_words('stringdata.csv', lang[0], lang[1]), lang[0]

テストコードの内容としては、対象領域に描画されている文言をPyOCRで読み取って、前述の文言ファイルの文言がその中で描画されているかチェックしています。

なおTwitter UIは文言描画がシンプルで明瞭なため、文言中の不正なスペース混入を除去する処理が必要な以外は、Tesseract OCRが提供するtessdata_bestの学習データをそのまま利用して十分な精度を確保できました。

テスト実行結果

テストを正常なキャプチャ画像で実行すると、テスト成功の結果が得られます。
次にキャプチャ画像を加工して、ドイツ語文言「Barrierefreiheit, Anzeige und Sprachen」の右端に文字切れを発生させてテストを実行すると、次のようにテスト失敗報告と、失敗原因となった文言を表示します。

pytest test_display_all_words.py -s
===================================================== test session starts =====================================================
(中略)                                                              

test_display_all_words.py failed: [Barrierefreiheit, Anzeige und Sprachen] is not found
F

========================================================== FAILURES ===========================================================
_______________________________________________________ test_all_words ________________________________________________________

    def test_all_words():
        '''各言語ごとの表示文言のチェック
        '''
        lang_list = [['eng', 0], ['jpn', 1], ['deu', 2]] #[lang_name, column_index of csv]
    
        for lang in lang_list:
>           assert check_all_words('stringdata.csv', lang[0], lang[1]), lang[0]
E           AssertionError: deu
E           assert False
E            +  where False = check_all_words('stringdata.csv', 'deu', 2)

test_display_all_words.py:39: AssertionError
=================================================== short test summary info ===================================================
FAILED test_display_all_words.py::test_all_words - AssertionError: deu
====================================================== 1 failed in 3.98s ======================================================

このアプローチの課題と対応方法

OCRを使った文言描画のテストの自動化には、利点・欠点それぞれあります。
結論を先に言うと、利点を活かし、欠点を補完するテストアプローチの工夫が求められます。

テストの偽陽性偽陰性の課題

まず欠点の1つ目ですが、テスト結果判定に機械学習を使用することから、テストの偽陽性偽陰性の問題に遭遇します。

このうち偽陽性については、テストが失敗したら本当に失敗なのか追加確認する運用で、安全方面に倒せます。

しかし偽陰性の方は問題です。若干の描画欠けが発生しても、機械学習エンジンが頭良く補完してテストを成功させてしまう場合があります。そのため、次のようなテストのアプローチの工夫が求められます。

  • 全文言の包括的なテストを、今回のOCRベースのアプローチで実装する
  • 継続的テストとしてのリグレッションテストにも、今回のOCRベースのアプローチで実装する
  • 一方、リスクレベルの高い文言描画については、適時のタイミングで、人による目視確認や、あるいは画像マッチングベースのアプローチでテストを実現する

描画座標依存によるFragile Testの課題

2つめの欠点ですが、次の要因から、今回のテストアプローチは対象画面の構成(レイアウトや描画位置座標)に依存します。

  • キャプチャ画像をそのままTesseract OCRに丸投げしても、複雑なレイアウト構成や、アイコン・境界などの意匠、透明化・影付きなどの効果といった、様々な要因で上手く描画文言を抽出できません。これに対策するためには、今回提示したテストコードでも実行していますが、対象領域をトリミングして、それぞれの領域に適した前処理や解析結果補正処理を実行する必要があります。すなわち、テストがレイアウトや描画位置座標に依存せざるを得ません。
  • キャプチャ画像の取得のため画面遷移の自動操作が必要です。全てを全自動にする場合、画面遷移操作をテストコード上に実装する必要があります。

上記の理由から、今回のテストアプローチでは、画面仕様や画面遷移仕様が変更される度にテストが壊れる場合があるという、Fragile Test問題に直面しがちです。

この対策として、例えば次のような工夫が求められます。

  • ソースコードから画面仕様や画面遷移仕様を解析してテスト操作を生成する処理を自動化する
  • テスト対象の限定。仕様が安定している画面の特定領域に限定してこのテストを適用する