回帰テストを生成するプログラムを作った話

# はじめに
はじめまして。ドイツの重機メーカーでソフトウェア開発をやってる、ぬると申します。
今回は職場で作ったテスト自動化ツールについてメモ (｀・ω・´)

# 背景
弊社技術部ではクレーンの制御ソフトをローコード開発環境でプログラムして実装しています。機械工学、電気工学出身の同僚エンジニア達がオペレータと呼ばれるブロックを組み合わせ、仕上がったら開発環境でテスト→諸々ビルドしてクレーンのコンピュータにフラッシュする流れです。雰囲気はPLCプログラミングに似ているかも。

今回は上司に、出来上がったオペレータを単体テストするプログラムを書けと依頼をもらったので、目的のオペレータを読んで回帰テストを生成するプログラムを作ってみました。

## 回帰テストとは
まず、ソフトウェアにおける回帰（Regression）とは、バグの一種で、俗に先祖返りと呼ばれるヤツのことです。[^1] 通常、新しい機能を追加したあとなど、コードに何らかの変更を加えたあとに、元々あった機能が意図したとおりに動作しなくなることを回帰と言います。[^2]

回帰テストとは、こういった先祖返りを検出するために作られたテストやその実行を主とするテスト手法のことを言います。多くの場合はブラックボックステストと類語で、あらかじめ用意した入力とそれに応じて期待される出力が、ソフトウェアを変更する前と後で概ね一致することを確かめます。

![regression_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3056786/ce845ee3-a0b5-1af0-7cd2-d503081ea410.png)

このテスト手法のメリットの一つに、テストするソフトウェアの中身を知らなくていいというのがあります。弊社の場合、~~同僚たちが大量の入出力端子が備わったウンコードならぬスパゲッティオペレータを創出するので~~ 単体テストを作成するのにいちいちオペレータの中身・仕組みや振る舞いを聞きに行かずに済みます。

逆にデメリットは中身の考査を行わないことにあります。何を入力に受け取っても出力が一定の仕様の場合も先祖返りを起こしているかは確認できません。ランダムな入力値を与えれば、オーバーフローやゼロ除算、無限大を叩き出したりするかも。

## 回帰テスト生成プログラム

今回は要件ベースの単体テストではなく、既に確立されたオペレータたちの現状の振る舞いを記録するための単体テストを作るのが目的なので、回帰テストを生成するツールを作ってテストの生成と実行までを自動化します。

![operators.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3056786/34e84fd7-56c3-e444-d338-87c37e24dad8.png)

幸い、開発環境にオペレータをテストする機能がついているので、テストデータを与えるだけで単体テストは開発環境がやってくれます。つまり、テストデータを生成するだけで済むのです。✌('ω') ✌ｲｴｰｲ。


### テストファイルの形式

弊社のローコード開発環境で作られるオペレータたちはXMLで記述され、ファイル単位でバージョン管理システムにて保管されています。今回はPythonでそれらXMLを構文解析して入出力のデータ型を読み出し、それらをもとに、オペレータごとにテスト用データセット(csv)を生成します。件のローコード開発環境は、独自のファイル形式とcsvをテストインプットとして承るので、生成しやすいcsvを選択した次第。

### テストデータの選出

オペレータの入出力に使われる変数はC言語で使われる基本的なデータ型と概ね同じです。int型が符号ありなし、様々なビット数のものが用意されてるのと、みんなだいすきfloat型、bool型、配列、構造体などがあります。開発者が独自に定義したデータ型(typedef的なの)などもありますが、大抵の場合は先に挙げた基本データ型を組み合わせて定義されています。これらの入出力に代入する用に、あらかじめJSONファイルにサンプルの値をまとめて用意しておきます。

```json:sample.json
{
   'uint8': [0, 5, 32],
   'uint16': [0, 12, 65],
   'uint32': [0, 114, 514, 810, 4444444],
   'int8': [-64, 0, 32, 127],
   'int16': [-256,0, 11, 2675, 32767],
   'int32': [-2147483648, -1234, 0, 65536, 114514],
   'float32': [-24.15, 0.0, 3.1415, 666666.666],
   'bool': [true, false],
}
```
オペレータの入出力を読み取ったら、データ型に応じてこのサンプルのプールから適当に値を選べばいいということですね。構造体やtypedefに出くわしたときには再帰的にこのプールの値にアクセスします。

`'T_CanIOModule': (Ix_value1: bool, Ii_value2: int32, Ir_value3: float32)`みたいな構造体があれば、プールにアクセスして`'T_CanIOModule': [(false, 65536, -24.15)]`のようなエントリを追加するか一時的に生成します。

### テストファイルの生成

今回の実装では、Pythonスクリプト内でpandasデータフレームを作り、それにプールの値を羅列（デカルト積）させてcsvに出力します。pandasマジ便利。入出力変数の数が多くなったりプール内で使える候補の数が多いと組み合わせの生成や実際のテストに時間がかかるため、

1. 一部の入力変数をゼロのままにしておく
1. デカルト積の結果からランダムにデータ候補を間引きする

といった手法で対処します。

### テスト実行

今、生成した状態のテストファイルにはランダムな入力値しか書かれておらず、オペレータがこれらを受け取ったときに何を出力するのかわかっていない状態です。それでもとりあえずこのままテストファイル（入力データ）をテストにかけます！...すると、開発環境が「オイ！実際の出力と期待してる出力が全然違うやろがい！」って文句を言ってくれるので、さらに実際の出力として出た値を読んで生成したテストファイルの期待出力データ欄に書き出します（天才？）。回帰テストの完成です。

### GUIとCLIを両方備える

弊社エンジニアたちは機械工学や電気工学出身の人たちが多く、ITおんちも少なくないため直感的に操作しやすいGUIをTkinterで作りました。テストファイル生成→そのままテスト実行→実際出力を期待出力欄に書き出し、の流れは回帰テストを知らない人たちにはわかりにくいと思うのでその辺は説明する必要が発生します。

もうわかったし、GUIなんていらねぇ！っていう、テキパキ情報を入力してテスト生成するよという人のためにはargparserを使ったCLIを用意しました。ここでは両方のインタフェースが同じライブラリの関数にアクセスする、[モデル・ビュー・コントローラ](https://ja.wikipedia.org/wiki/Model_View_Controller)というデザインパターンを参考にしました。

### このツールのさらなる用途

今回は色々なオペレータ（ソフトウェアモジュール）にそもそもテストが備わっていないため、簡単に現状の振る舞いを把握・テストするために回帰テストを生成してきましたが、これらの回帰テストはCI（[継続的インテグレーション](https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%86%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)）の実装に役立つかと思われます。例えば、オペレータの改変がgitでコミット/プッシュされたときに、Jenkinsなどを使ってプッシュされた改変を自動的に、あらかじめ作っておいた回帰テストにかけたりできます。もし改変がこのテストをパスできれば改変前より最適化されたと判断できますし、全く違う内容を出力して、パスできなければ中身の精査が必要だとわかります。入力や出力が増えたり減ったりすればテストを定義し直す必要はありますけどね〜。

うちの課でもCIシステム構築の機運が高まっているので、このツールを使って構築に助力できればいいな。

# あとがき

基本的なことしか書いてきませんでしたが、この記事が誰かの役に立つといいなぁと思い投稿しました。もしテストデータ生成やテスト手法、アプローチに改善の提案などあれば、コメントしていただけると非常に喜びます。最後まで読んでいただきありがとうございました。


[^1]: 多変量解析や機械学習における回帰とはちがうよ
[^2]: Vladimir Khorikov著 Unit Testing: Principles, Practices and Patterns