🥷

自動テストの内容が分からなくなる 6つの原因とそれを解決する diff patch-o ツール

2023/03/12に公開

(2023-03-21 更新 多段パッチを追加)

自動テストの内容が分からなくなる原因

自動テストを開発したはいいですが後日あらためて自動テストのコードを見たら何をテストしているのか分からなくなったことはありませんか。 最近自分が作ったテストなら分かるかもしれませんが他の人が作ったテストは分かるでしょうか。 単体テストであれば、いくつかパラメーターを見るだけで分かるかもしれません。 しかし、テストで最も重要な結合テストや総合テストを自動化した場合、どんなデータなのか分からなくなることが多いと思います。

具体的にイメージしてもらうために、サンプルを示しましょう。 HTML を テスト データ にするケースは稀ですが多くの人が知っているので、イメージしやすいでしょう。 Web サイト をスクレイピングするプログラムなら、テストの入力データになります。

<!DOCTYPE HTML>
<html><head>
<!-- Character Code Encoding: "WHITE SQUARE" is □ -->
<meta charset="UTF-8">
<title>No title</title>
</head><body>

<p id="case">
HTML example
</p>
</body></html>

HTML を全く知らない人なら、最初から読んで1つ1つ意味を調べていくことになり、なかなか本文を読むことができません。 知っている人なら、body まで読み飛ばして、すぐに本文を読むことができます。

<p id="case">
HTML example
</p>

HTML なら知っている人は多いでしょうが、開発しているプログラムが扱うデータは HTML ではないと思います。 しかし、JSON, YAML, XML, CSV などの形式のデータでも、こういったテストとは関係ないデータが多くの部分を占めていることでしょう。

テスト データ が分からない原因はいろいろあります。

  • そもそも 1つ1つのデータが大きく、ほとんどの部分のデータがテストとは無関係
  • 2つ目以降のテストケースのデータがほとんどの部分が同じ内容で読む気が失せ、読む気があっても違いを探さなければならない
  • データをコンパクトしてパーツに分けるために、プログラミング言語を駆使していて、解析しなければならない
  • Python の fixture のような、分かりにくいし定義にジャンプしにくいライブラリでパーツを分けている
  • 開発環境を構築しないと テスト プログラム を動かせず、データを表示できない
  • データが書かれたファイルのファイル名やパスが、プログラムを解析しないと分からない

そして、テスト データ が分からないといろいろな弊害が起きます。

  • ユーザーに近い人にデータ自体の正しさをレビューしてもらうことができず、データを信用できない
  • 修正しようにも、データ構造から分かっていないので、どこを修正したらいいか分からない
  • 共通部分のデータを修正しようとしたとき、影響範囲が分からないために修正が怖くなる
  • プログラムの関数などである程度パラメーター化されているが、パーツの範囲を広げるなどパラメーターになっていない構造を、どのように修正したらいいか分からない
  • プログラムを修正したら、既存のデータでさえ意図せず破壊(デグレード)してしまうことがある

TDD(テスト駆動開発)は積極的に行うべきですが、単体テスト(ユニットテスト)に関しては最近懐疑的になっています。多くの場合、要求仕様にない中間的な開発者独自定義のデータ型(たとえばデータ量が減っていないDTOなど)に対するテストをしていて、何が正しいかが分からず編集できなくなるからです。また、不具合があっても細かい部分なので重大な不具合にはなりません。もちろん細かい条件の違いの不具合によって全体が動かなくなるケースもありますがそのケースは少ないです。一方、結合テストや総合テストで失敗する不具合はシステム全体が使えなくなるため重大です。また、不具合が発生したとき、発生条件を絞り込まなくても全体のデータを渡して動作確認することができ、素早い復旧が期待できます。

これらの弊害を解決するのが後ほど紹介する patch-o ツールです。 単なる patch ツールだろ知ってるよ、と読み飛ばすのではなく実践していただくことが重要です。 もし、何でもできるプログラミング言語さえ知っていれば問題ないと考えているのであれば、私の経験上、上級者だと思っているあなた自身が上記の弊害を起こしている原因である可能性が高いです。

差分が テスト ケース を表す

そもそも 1つ1つのデータが大きく、ほとんどの部分のデータがテストとは無関係

1つ目のテストケース(1stケース)はシンプルなデータにするでしょう。シンプルなデータはテストの内容を表すデータというよりは、プログラムを動作させるためのデータです。 1stケースはどういった種類のデータであることさえ理解すれはよく、データの内容は理解していなくても大丈夫なのです。 ということは、1つ1つのデータが大きく、ほとんどの部分のデータはテストとは無関係であってもよいのです。 また、正解データは入力データの一部のコピーといったシンプルなものが多いので、検索して見つかる可能性も高いです。

たとえば、1つ目のテストケースの全体のデータが以下のようなデータだったとします。 HTML であることが分かれば、内容は読まなくていいです。

<!DOCTYPE HTML>
<html><head>
<!-- Character Code Encoding: "WHITE SQUARE" is □ -->
<meta charset="UTF-8">
<title>No title</title>
</head><body>

<p id="case">
HTML example
</p>
</body></html>

2つ目以降のテストケースのデータがほとんどの部分が同じ内容で読む気が失せ、読む気があっても違いを探さなければならない

2つ目以降のテストケースではどうでしょう。 2つ目以降のテストケースはこれまでのテストケースとは違うケースのテストです。 それも開発しているプログラムにとって意味のある違いがあるテストなので、テストデータの差分をツールで表示させれば、すぐにテストケースの内容を理解できるでしょう。 同じ内容の部分を読む必要はありません。

たとえば、2つ目のテストケースの全体のデータが以下のようなデータだったとします。 内容は読まなくていいです。

<!DOCTYPE HTML>
<html><head>
<!-- Character Code Encoding: "WHITE SQUARE" is □ -->
<meta charset="UTF-8">
<title>No title</title>
</head><body>

<p id="case">
HTML5 example
</p>
</body></html>

Linux の diff ツールで比較してみましょう。 次のように出力されます。 この出力を Visual Studio Code にコピペしても、色分けしてくれるので見やすいです。 もし色分けが変だったら、ファイル タイプ diff 形式 を選びます。

コマンド:

diff -u4rN 1.html 2.html

出力:

緑色の行頭が + の行を見て、HTML5 example が入力されたときのテストケースであることがすぐに分かりますね。

このように、テストデータは diff 形式で作るべきです。 Windows なら Git をインストールして Git bash を開くと実行できます。 ただし、1.html 2.html が何であるか(何の差分であるか)は後述するように分かるようにしなければなりません。

データをコンパクトしてパーツに分けるために、プログラミング言語を駆使していて、解析しなければならない。 Python の fixture のような、分かりにくいし定義にジャンプしにくいライブラリでパーツを分けている

diff 形式 であれば、プログラミング言語を駆使する必要はありませんし、Python の fixture を使う必要もありません。

もちろん、プログラム言語ほどの柔軟性はないので、diff では機能不足になることも考えられます。 そのときはシェルスクリプトや Python などのコードを書けば対応できますが、それが必要になったことは殆どありません。 もし書くときは、React コンポーネントなどを使って対象データの構造に合わせて部品化する必要はありません。 むしろしないほうが良いです。 注目すべき部分がどこにあるかが分かるように差分さえコードで定義できれば良いです。 言語はシェルスクリプトを推奨します。 Visual Studio Code の Bash Debug 拡張機能でデバッグできますし、言語特有のライブラリを使った書き方を学習しなくて済みます。 curl コマンドなどもそのまま使えます。

データが書かれたファイルのファイル名やパスが、プログラムを解析しないと分からない

この問題は、情報をプログラムからデータに移した時点で基本的に無くなります。 patch1 ファイルの中に、ベースとなるファイルの相対パスと、パッチを適用してできるファイルの相対パスが書かれているので、テスト プログラム を解析しなくても テスト データ が書かれたファイルの場所が分かります。 基本的に相対パスの基準パスは、相対パスが書かれているファイルがあるフォルダーのパスにします。 何らかの処理を実行しているときの カレント フォルダー であったら見つけることが困難になります。 もしファイルの場所が分からないとどういうデータであるか分からなくなってしまうでしょう。

開発環境を構築しないと テスト プログラム を動かせず、データを表示できない

diff 形式 のデータから既にテストの内容が分かっていると思いますが、もし、プログラムが入力する全体のデータを見たいのであれば、Linux の既存の patch ツール を使えば取得(出力)できます。 失敗した自動テストをデバッグするときはテスト内容に関係ない部分のデータであってもチェックが必要になるでしょう。 Windows なら Git をインストールするだけで使えます。 Linux も patch をインストールするだけで使えます。 CentOS7 なら sudo yum -y install patch です。 しかし、既存の patch には一部問題があります。

patch の問題点

Linux の patch ツールは、1stケースのテストデータような 1つのベースとなるファイルから、複数の個別のケースのファイルをそれぞれ出力することができません。 ベースとなるファイルのパスを書き間違えたのだろうと推測してエラーになったり、誤った出力をしたり、ベースのファイルを書き換えたりします

その問題を解決する patch-o スクリプトについては後で示しますが、一応検証してみましょう。 既存の有名なツールが使えるのであれば、そのほうが良いですから。 読者は本章を読み飛ばしていただいても構いません。

既存の patch の動きを実際に動かして確認したい場合は GitHub に置いた patch-o を git clone するか、GitHub の Code メニューから .zip ファイルをダウンロードして動かしてください。 手順の詳細は README.yaml の Standard patch command case を参照してください。

次のような差分を作ったとします。 base.json をベースに差分を適用した A.json ファイルを作るとします。

diff -u4rN "base.json" "build/A.json" >  "patch1"

出力される diff 形式の patch1 ファイルの内容は以下のようになります。

--- base.json
+++ build/A.json
@@ -1,5 +1,5 @@
 {
-    "ID": "A123456",
+    "ID": "A000001",
     "email": "a@example.com",
     "Name": "A"
 }

個別のケースのファイルをそれぞれ出力することは、base.json と patch1 から A.json ファイルを作ることなので、まずは A.json ファイルを削除します。

rm  "build/A.json"

準備は整いました。 既存の patch コマンドを実行してみます。

patch  "base.json"  -o "build/A.json" < patch1

成功しました。 patch の -o コマンドはこのケースでは期待通り動きました。

次に、base.json をベースに差分を適用した A.json ファイルと、base.json をベースに差分を適用した B.json ファイルを作るとします。

diff -u4rN "base.json" "build/A.json" >  "patch1"
diff -u4rN "base.json" "build/B.json" >> "patch1"

出力される diff 形式の patch1 ファイルの内容は以下のようになります。

--- base.json
+++ build/A.json
@@ -1,5 +1,5 @@
 {
-    "ID": "A123456",
+    "ID": "A000001",
     "email": "a@example.com",
     "Name": "A"
 }
--- base.json
+++ build/B.json
@@ -1,5 +1,6 @@
 {
     "ID": "A123456",
-    "email": "a@example.com",
-    "Name": "A"
+    "email": "b@example.com",
+    "Name": "B"
+    // dummy file date.json    2022-01-01 11:22:33.900000000 +0900
 }

複数の個別のケースのファイルをそれぞれ出力することは、base.json と patch1 から A.json ファイルと B.json ファイルを作ることなので、まずは A.json ファイルと B.json ファイルを削除します。

rm  "build/A.json"
rm  "build/B.json"

準備は整いました。 既存の patch コマンドを実行してみます。

patch  "base.json"  -o "build/A.json" < patch1

GNU(Ubuntu20.04, CentOS7) の場合、build/A.json には A.json の内容と B.json の内容の両方が出力されてしまします。
BSD(macOS 13) の場合、なんと、base.json ファイルの内容が書き変わってしまいます。

2つのファイルの patch なのに 1つのファイルを -o オプションに指定しているのが悪いようなので、フォルダーを指定してみます。

patch -i patch1  -o  build

GNU の場合、フォルダーが指定されたとしてエラーになり build/A.json は出力されません。
BSD の場合、同じく base.json ファイルの内容が書き変わってしまいます。

patch < patch1

も同様です。

残念ながら現状の patch ツールでは複数のファイルに差分を適用するときは、1つ1つファイルを指定しなければなりません。

patch-o ツール

複数のファイルにパッチ当てる

現状の patch ツールでは複数のファイルに差分を同時に適用することはできませんが、1つ1つファイルを指定して何度も patch ツールに -o オプションを指定して実行すれば、期待通りの動きになることが分かりました。 そこで、そのような動作をするスクリプト patch-o を開発しました。 GitHub に置いた patch-o を git clone するか、GitHub の Code メニューから .zip ファイルをダウンロードして動かしてください。 手順の詳細は README.yaml を参照してください。 diff 形式のファイルの解析が必要なので多少複雑になっていますが、動作仕様は単純です。

次のような差分を作ったとします。 base.json をベースに差分を適用した A.json ファイルを作るとします。

diff -u4rN "base.json" "build/A.json" >  "patch1"
diff -u4rN "base.json" "build/B.json" >> "patch1"

出力される diff 形式の patch1 ファイルの内容は以下のようになります。

--- base.json
+++ build/A.json
@@ -1,5 +1,5 @@
 {
-    "ID": "A123456",
+    "ID": "A000001",
     "email": "a@example.com",
     "Name": "A"
 }
--- base.json
+++ build/B.json
@@ -1,5 +1,6 @@
 {
     "ID": "A123456",
-    "email": "a@example.com",
-    "Name": "A"
+    "email": "b@example.com",
+    "Name": "B"
+    // dummy file date.json    2022-01-01 11:22:33.900000000 +0900
 }

複数の個別のケースのファイルをそれぞれ出力することは、base.json と patch1 から A.json ファイルと B.json ファイルを作ることなので、まずは A.json ファイルと B.json ファイルを削除します。

rm  "build/A.json"
rm  "build/B.json"

準備は整いました。 patch-o を実行してみます。

./patch-o  "patch1"

期待通り A.json ファイルと B.json ファイルが作られました。

既存の A.json, B.json ファイルと内容が変わるときは、変えられる前にユーザーに確認を求められます。 確認が不要な場合は -f オプションを指定してください。

./patch-o -f  "patch1"

複数のファイルからパッチを更新する

patch ファイルを作るときは 1つ1つのファイルに対してそれぞれ diff コマンドを実行していましたが、patch ファイルに書かれたすべてのファイルについてパッチの内容を更新するときは patch-o に -R オプション または --reverse オプションを指定したコマンドを 1回実行するだけでできます。

./patch-o -R  "patch1"

または

./patch-o --reverse  "patch1"

更新した後の内容が書かれた A.json ファイルと B.json ファイルがあるときに実行すると、patch1 ファイルを更新します。

パッチの基準パス

相対パスの基準パスは patch1 ファイルがあるフォルダーのパスです。 下記の場合、カレント フォルダー の親フォルダーが基準パスになります。

./patch-o  "../patch1"

パッチを適用してできるファイルを別の場所に作る場合は入力をリダイレクトします。 その場合、patch1 ファイルがある場所に関わらず カレント フォルダー がパッチを適用してできるファイルの基準パスになります。 ベースとなるファイルの基準パスは patch1 ファイルがあるフォルダーのパスから変わりません。

./patch-o < "../patch1"

パッチを適用したファイルは .gitignore に入れる

パッチを適用したファイルとパッチは同じ情報が含まれているため、その両方のファイルがある状態というのは一元化されていない状態ということになります。 パッチを適用したファイルを .gitignore によってリポジトリに入らないようにすることで、どちらがマスターであるかが明確になります。 リポジトリに入れるプロジェクト全体のファイルサイズも減ります。 ただし、自動テストを実行するときは、パッチを適用する処理も行わなければなりません。

多段パッチ

base.json → A.json の差分と A.json → AA.json の差分から AA.json を作るといった多段の関係についてもサポートしています。 base.json → A.json → AA.json という関係です。 A.json が作られる前に A.json → AA.json のパッチをあてようとして A.json ファイルが無いエラーになることはありません。

Discussion