テスト仕様書をGitで管理する: MarkdownからExcelを作る

テスト仕様書、書いていますか？

会社によってテスト仕様書として作ったり、テストコードとして作ったり、いろいろやり方はありますね。
大きめの歴史ある会社では、Excelファイルのテスト仕様書を作ることがよくあると思います。
複数人で編集するときは、 `_最新`, `_最新_20230103`, `_最新_5` などの接尾辞を駆使してバージョン管理を行う場合もよくあると思います。

Excelファイルの取り扱いのベストプラクティスがあればそれはそれで知りたいですが、いまのところ名案がないので、プログラムを使ってGitリポジトリで差分を見えるようにして管理することを考えました。

しかしExcelファイルをそのままGitに入れたのでは、差分もよくわかりません。 TortoiseGitなどでは差分も表示してくれたりしますが、プルリクエストやそこへのコメントってできるのでしょうか。 だいたいはWebページ上で差分を見るわけですし。
かといってCSVでもちょっとイマイチです。表形式で編集できるツールはありますが、リポジトリのdiff表示では単純なキャラクタ区切りのテキストになるはずで、見やすいとは言えません。
そこで、Markdownでテストケースを管理することを考えました。 テキストで管理できれば *nix のコマンドも使えます。 Excel持ってない人でもテストケース書けますし、 Excelに変換できるようにしておけば誰も困りません。 (使用したツールでは CSV, TSVへの変換もできます。)

## ここまでの整理

* Gitで管理できれば最新バージョンがどれか迷わない。
* Markdownで管理できればプルリクエストで差分が見える。そこにコメントもできる。
    * CSVでは表示が崩れることもある
* Markdownで管理できれば *nix のコマンドが使える。
* Markdownで管理できれば Microsoft Excel 持っていない人でも作成・編集できる。
* MarkdownからExcelに変換できれば 表形式を使用したい時にも困らない。

## 試した環境

* macOS Monterey Version 12.6
* Python 3.10
* meal 0.0.3.32

## マークダウンをExcelにする流れ

### 準備

[mael](https://pypi.org/project/mael/) という Python のツールをインストールします。

```
$ pip install mael
```

テスト仕様書を格納するディレクトリを作成します。

```
$ mkdir test
```

フォルダを初期化します。
どのテンプレートを使うか尋ねられます。
テストの場合は `1: Test case` を使うと便利です。

```
$ mael init test
Initialize /directory/path/test.
Which template do you use?

0: Normal
1: Test case

Type number: 1[Enter]
```

すると、次のようにファイルが生成されます。

```
|--.gitignore
|--config
|  |--columns.yml
|  |--ignore.txt
|  `--variables.ini
|--README.md
|--Scenario 1.md
`--Scenario 2.md
```

`config`内には各種設定ファイルがありますが、後で変更できるので、メインのテストケースの作成に進みます。

### テストケース記述

`Scenario 1.md`, `Scenario 2.md` にはサンプルのテストケースが記述されています。
これを更新してみましょう。

`vim test/Scenario\ 1.md` などで編集します。

```md:Scenario 1.md
# Scenario 1
## Summary
これはテストです。

## List

### 項目
ログイン確認
### 説明
ログインしてみる
### 期待値
ログインできる

---

### 項目
マイページを開く
### 説明
右上のリンク「マイページ」をクリックする
### 期待値
マイページが表示される。
次の４つのメニューが表示される。
* メールアドレス変更
* パスワード変更
* 住所変更
* 退会
```

内容は何でもいいですが、とりあえずテストっぽく書いてみました。

### Excelファイル生成

次のコマンドを実行します。

```bash
$ mael build test
```

`test/output/test.xlsx` が生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/a5f347e2-a294-11f6-e02c-7083d4669647.png)

`config`ディレクトリ内の設定により、 Result, Timestamp, Comment の列が追加されています。
テスト実施時に、実施時刻、コメントを入力する欄として使えます。

「項目」、「説明」、「期待値」の列幅を調整するには `columns.yml`を変更します。

```yml
column_conditions:
  項目:
    width: 20
  説明:
    width: 30
  期待値:
    width: 30
```

(既に `column_conditions` の中には Categories, Description, Expected の列設定がありますが、使わないので削除して構いません。)

そして再びコマンドを実行します。
すると下のように列幅が変更されて出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/050d0df6-7b94-4ab7-e4a6-9b3436507b78.png)


### 最後のステップ

Markdown のファイルは Git に格納しておきましょう。
まだ初期化していませんでしたから、 `git init` から行います。

```bash
$ git init
$ git add .
$ git commit -m initial
$ git remote add origin https://...
$ git push -u origin
```

これで、テストケースをマークダウンファイルで管理できるようになりました。

[mael](https://pypi.org/project/mael/) はマークダウンのフォーマットを合わせておけばExcelの表を出力できるので、テスト仕様に限らずいろいろなデータをマークダウンで管理するのに役立ちます。

## 便利技

### 変数を使う

何度も出てくるユーザ名やURLなどは繰り返し使えるように変数にしてしまいましょう。
Excelだったらセル参照や名前の定義を行いますね。

[mael](https://pypi.org/project/mael/)では、 `config/variables.ini` で変数を定義します。 いや定数ですが。

```ini:config/variables.ini
URL=https://sample.com
USERNAME=admin
PASSWORD=abcdefg
```

定義した変数を使うには、変数名を `{{` `}}` で囲みます。

```markdown:Scenario 2.md
# Scenario 2
## Summary
{{ URL }} を開き {{ USERNAME }} / {{ PASSWORD }} でログインします。

## List
### Test
{{ URL }} を開き {{ USERNAME }} / {{ PASSWORD }} でログインします。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/2c425558-5034-2191-dee2-73a6e4ccb269.png)

### 環境ごとに変数の値を変える

本番環境と開発環境でウェブサイトのURLが違う、使用するユーザ名が違うなど、環境ごとに変数の値を変えたい時があります。
その場合は、 `variables.{環境名}.ini` という形で変数の定義を作成します。

たとえば次のように、`variables.dev.ini`を作成します。

```ini:config/variables.dev.ini
URL=https://dev.sample.com
```

そして オプションパラメータ `-e` を使ってコマンドを実行します。

```
$ mael build test -e dev
```

これにより `test/output/test_dev.xlsx` が作成されます。
`variables.ini` は使用されますが、 `variables.dev.ini` に同じ変数の定義がある場合は、 `variables.dev.ini` の定義が優先されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/312d81f2-3aa2-9596-2da0-1fd5686500d6.png)

### Markdownの行数を減らす

表の全項目が縦に並びますから、Markdownファイルの行数は長くなります。
Markdownの行数を減らすには、次の2つの方法が使えます。

#### `---` を使わない

上では `---` を使って次の行に移るという説明をしました。
これはMarkdownでの見え方を意識したものであって、必ずしも必要ではありません。
同じカラムの値が続くと、2回目のものは新しい行の値として扱われます。

```Markdown
## List
### Column
A
### Column
B
```

| Column |
|:--|
| A |
| B |

新しい行ではなく、既存の行の上書きをするような設定も可能です。

#### ブランクの場合にコピーする

テスト仕様書含むExcelの表では、特定のカラムについて1行目と2行目が同じになることがあります。
次の表では大分類と中分類が繰り返しになっています。

| 大分類 | 中分類 | 小分類 | 備考 |
|:--|:--|:--|:--|
| 東京都 | 中央区 | 銀座 | TEXT1 |
| 東京都 | 中央区 | 築地 | |
| 大阪府 | 北区 | 梅田 | TEXT2 |

この場合、`config/columns.yml` の設定によって、 繰り返し部分を1回の記述にすることができます。 次の例では、 大分類「東京都」、中分類「中央区」を1回しか記述していません。

```markdown
## List
### 大分類
東京都
### 中分類
中央区
### 小分類
銀座
### 備考
TEXT1

### 小分類
築地

### 大分類
大阪府
### 中分類
北区
### 小分類
梅田
### 備考
TEXT2
```

`config/columns.yml` では複数の設定方法があります。

全体設定で、空白の場合に前の行をコピーするようにして、備考ではコピーしないように設定する方法、

```yml
global:
    duplicate_previous_for_blank: true

column_conditions:
    備考:
        duplicate_previous_for_blank: false
```

大分類と中分類のみ、空白の場合に前の行をコピーするように設定する方法、

```yml
column_conditions:
    大分類:
        duplicate_previous_for_blank: true
    中分類:
        duplicate_previous_for_blank: true
```

`config/columns.yml` では全体の設定のみ行い、「東京都」「中央区」「築地」の「備考」はタイトルのみ書く方法です。
最後の設定ではマークダウンファイルも少し変更が必要です。

```yml
global:
    duplicate_previous_for_blank: true
```

```markdown
...

### 小分類
築地
### 備考

...
```

### CSVで出力する・TSVで出力する

結局CSV?と思ってしまうかもしれませんが、CLIの中で確認するにはCSVも捨て難いフォーマットです。
`mael build` を実行するときに `-f` オプション を使うと CSV・TSVで出力できます。 デフォルトは `-f excel` です。

```bash
$ mael build test -f csv
$ mael build test -f tsv
```

### 既存のExcel手順書をMarkdownにする

[mael](https://pypi.org/project/mael/)を使うためには仕様書がマークダウンになっている必要があります。
とはいえ、既存の仕様書を人手でMarkdownに書き起こすのはだいぶ大変です。
そこで、pandasを使ってMarkdownに変換します。

`read_excel`を使うとExcelが読み込めますから、それをMarkdownとして出力します。

#### サンプルコード

私はjupyter notebookとpandasを使って処理しました。

まずはデータを読み込みます。

```python
import pandas as pd

sheet_name = "test_steps"

df = pd.read_excel(
    'test.xlsx',
    sheet_name=sheet_name, # シートの指定がある場合
    header=5,              # ヘッダの行
    # 使用するカラムに指定があればここに記述
    usecols=[
       'ID', '大分類', '中分類', '小分類', '説明', 'URL',
       '正常系／異常系', '予想結果', 'コメント'
    ]
)
```

データを標準出力に出力して確認します。

```python
d = df.fillna('').to_dict(orient="record")
l = len(d)

def write(name, d, f=None):
    print("#", name, file=f)
    print("##", "Summary", file=f)
    print("##", "List", file=f)
    for i in range(len(d)):
        h = d[i]
        for k, v in h.items():
            print("###", k, file=f)
            if len(str(v)) > 0:
                print(v, "\n", file=f)
        print("\n", file=f)
        print("---", "\n", file=f)

write(sheet_name, d)
```

最後に、ファイルに出力します。

```python
with open('exported.md', "w") as f:
    write(sheet_name, d, f=f)
```

### Excelには入れない、 README.md ファイルを置いておきたい。

`config/ignore.txt` を作成して、 `README.md` を記述してください。

```txt
README.md
```

Markdown のファイルはすべてテストシナリオとして読み込むので、別にしておきたいものがあれば `ignore.txt` に入れてください。

## Markdown による Excel テスト仕様書 の作り方 まとめ

1. `mael init test_dir` で初期化
1. マークダウンファイルを編集してテストケースを作成
    * Summary, List を記述する。
    * 行は `---` で区切ると Markdown としても見やすい。 行数を減らしたいなら省略可能。
    * 変数を使うなら `test_dir/config/variables.ini`, `test_dir/config/variables.{環境名}.ini` を使う。
    * 列幅を調整するなら `config/columns.yml` を使う。
1. `mael build test_dir`, `mael build test_dir -e {環境名}` でExcel作成
    * `-f csv`, `-f tsv` で CSV/TSV にも変換可能 

## その他

今回は [mael](https://pypi.org/project/mael/) というツールを使いましたが、 もっと良いツールがあればぜひ教えてください🙇‍♂️
Gaugeもありかなと思ったのですが、非エンジニアが見るにはつらいのかなと思ったりもしました。
Pandocもひとつかと思いましたが、Pandocがテスト仕様書に向いているかはいまいちわかっていないです。
[mael](https://pypi.org/project/mael/) には、図を挿入する機能はないので、 Pandoc もありだとは思うのですが、そもそもきちんと言葉で書くべきなので図がなかったところで問題にはならないとも思います。 ブランク部分に上の行の値をコピーする機能は Pandoc にはないと思いましたから、その点は [mael](https://pypi.org/project/mael/) のほうがいい気がします。