Pythonで画像のビジュアルリグレッションテストを書く
小ネタです🍣
仕事でDjangoでできたWebアプリケーションの開発をおこなっているので、Pythonのバージョンのアップデートを継続的におこなう必要があります
当アプリケーションは、特定のエンドポイントが期待するレスポンスを返すかとか、共通モジュールが適切に動作するかといったテストがそれなりに書かれており、
普段の機能開発に限らずバージョンアップにおいても重要な役割を果たしていますが、
例えば画像生成のような、OSにインストールされたライブラリへの依存度が高い機能については、それらと異なる観点でのテストが必要になるものと思います
例えば、以下のようなユーザーの公開URLに対して固有のOGP画像を定期的に更新して生成している…といったようなことです
TwitterにおけるOGP画像の表示イメージ
画像生成のテストを、画像が生成できるかのような適当なアサーションで済ませていると、
例えばフォントが読み込めなくなってTOFUが表示されてしまっているとか、デザイン崩れが発生しているといったデグレードに気づけません
そのような時に役立つ、ビジュアルリグレッションテストの書き方のメモです✍
作り方
Pillow を用意します🛌
(画像処理をする場合、依存関係で既にインストールされているかもしれませんが)
テストを格納するディレクトリには、テストコードに加えて、正解となる画像ファイル expected.png
をコミットしておきます
これは、テストを始める段階で実際のコードから出力した内容になります
今回は簡便のためテストケースをひとつだけにしていますが、複数のテストパターンを用意する場合はそれに合わせて格納してください
$ tree .
.
├── .gitignore
├── expected.png
└── test_ogp_creator.py
.gitignore
では、テスト実行時に出力される actual.png
と diff.png
をignoreします
actual.png
diff.png
テストコードは以下のようになります
from os.path import dirname, join
from unittest import TestCase
from PIL import Image, ImageChops
# テスト対象のクラス
from hogehoge.ogp import OgpCreator
class OgpCreatorTest(TestCase):
def test_generate_ogp(self):
actual_file_path = join(dirname(__file__), 'actual.png')
expected_file_path = join(dirname(__file__), 'expected.png')
diff_file_path = join(dirname(__file__), 'diff.png')
params = {
# 省略
}
ogp_creator = OgpCreator()
result = ogp_creator.generate_ogp(params)
self.assertIsNotNone(result)
# actual.png を保存
with open(actual_file_path, 'wb') as f:
f.write(result)
# expected.png と actual.png を比較する
actual_image = Image.open(actual_file_path)
expected_image = Image.open(expected_file_path)
diff = ImageChops.difference(actual_image, expected_image)
if diff.getbbox():
# 差分ありの場合は、 diff.png を出力しエラーとする
diff.save(diff_file_path)
self.fail(msg=f'{actual_file_path} と {expected_file_path} を比較し、差分を検知しました。'
f'{diff_file_path} を確認し、'
f'想定された差分の場合は expected.png を actual.png で上書きしてください')
正答である expected.png
と、テスト環境で今回生成した actual.png
について ImageChops.difference()
でピクセル単位の差分を抽出した後、 Image.getbbox()
で画像のバウンディングボックス を取得します
両画像で差分が全く無かった場合は画像が空ファイルとなり、その時のバウンディングボックスは None
が返却される仕様のため、if文で簡単に確認できます🍮
差分があった場合はその画像をファイル出力すれば、具体的にどこが変わったか確認できます👁
動作
テスト実行時のイメージを示します
今回は、テストが落ちた時の状況を示したいため、以下のようにわざとグレーの四角形で一部を塗りつぶした画像を expected.png
として配置してみます
expected.png
テストを実行してみます
$ python test_ogp_creator.py
(略)
F
======================================================================
FAIL: test_generate_ogp (test_ogp_creator.OgpCreatorTest)
----------------------------------------------------------------------
(略)
AssertionError: /xxx/actual.png と /xxx/expected.png を比較し、差分を検知しました。/xxx/diff.png を確認し、想定された差分の場合は expected.png を actual.png で上書きしてください
----------------------------------------------------------------------
Ran 1 test in 0.317s
FAILED (failures=1)
Preserving test database for alias 'default'...
actual.png
は以下のようになりました
今回は、これが本来想定した問題ない出力結果であるとします
actual.png
expected.png
と actual.png
の差分を表現する diff.png
は以下でした
グレーに塗りつぶした部分が差分として表示されており、正しく差分検知ができていることがわかります
diff.png
目視の結果、 actual.png
が正しい結果であることが分かっているため、メッセージで示された通りに expected.png
を上書きします
再度テストを実行すると、今度は差分がないため正常終了します🍕
$ cp actual.png expected.png
cp: 'expected.png' を上書きしますか? y
$ python test_ogp_creator.py
.
----------------------------------------------------------------------
Ran 1 test in 0.252s
OK
そんだけ😌
Discussion