テンプレートライブラリを用いてpytestのコードを生成する

# 実現したいこと
:::note
自動テストコードをなんとか自動で生成したい！
:::
現在、モデルベースドテストやキーワード駆動テストなど「説明可能性の高いテスト」に関する技術について調査を進めています。

そのような技術の一つのゴール地点である「自動テストコードが自動で生成され、自動で実行される」を目指すために、如何にテストコードを生成するかを検討しました。
上流部分（モデルやキーワードからどのようにテストケースを導出するか）は別途検討中のため今回は触れません。

# 実装内容
「あらかじめテンプレートを設定しておくと、渡されたデータに応じたテストコードを生成してくれる」という仕組みになります。
Pythonのコードやテストコードはお試しで作ったものなので、ご参考までに！
### ディレクトリ構成
```
testcode-gen/
    ├ .gitignore
    ├ poetry.lock
    ├ pyproject.toml
    ├ src/
        ├ main.py
        ├ templates/
            ├ templete.py.mak
        ├ outputs/
            ├ test_sample.py
```
### 1. Makefileでテストコードのテンプレートを作る
Makefileは、pytest-bdd（templetes/test.py.mak）を参考にさせていただきました。
このテンプレートが呼び出されると、下記のような処理が実行されます。
- 渡されたシナリオの数だけテストメソッドを作る
- 渡されたコードをテストメソッド内に転記する

https://github.com/pytest-dev/pytest-bdd/blob/master/src/pytest_bdd/templates/test.py.mak
```makefile

from playwright.sync_api import Page


class TestSample:

    % for scenario in sorted(scenarios, key=lambda scenario: scenario.id):
    def test_${scenario.id}_${scenario.name}(self, page: Page):
        % for src in scenario.src:
        ${src}
        % endfor

    % endfor
```

### 2. テストコードを生成するためのスクリプトを実行する
makoというテンプレートライブラリを使用します。
同様の処理を実現できるライブラリは他にもあるようですが、こちらもpytest-bddのgeneration.pyにならって実装しています。

https://pypi.org/project/Mako/

https://github.com/pytest-dev/pytest-bdd/blob/master/src/pytest_bdd/generation.py

```python
from mako.lookup import TemplateLookup
import os
from dataclasses import dataclass, field


@dataclass
class ScenarioData():
    name: str
    id: int
    src: list = field(default_factory=list)


if __name__ == "__main__":
    # テンプレートを参照する
    template_lookup = TemplateLookup(
        directories=[os.path.join(os.path.dirname(__file__), "templates")]
    )
    template = template_lookup.get_template("template.py.mak")

    # テストシナリオを定義する（コードをベタ書きしているが、モデルやキーワードから生成される状態を目指したい）
    sd_1 = ScenarioData(name="goto_main", id=1, src=[
        "page.goto('https://playwright.dev/')",
        "title = page.title()",
        "assert title == 'Fast and reliable end-to-end testing for modern web apps | Playwright'"
    ])
    sd_2 = ScenarioData(name="goto_doc_intro", id=2, src=[
        "page.goto('https://playwright.dev/docs/intro')",
        "title = page.title()",
        "assert title == 'Installation | Playwright'"
    ])
    sd_3 = ScenarioData(name="goto_doc_api", id=3, src=[
        "page.goto('https://playwright.dev/docs/api/class-playwright')",
        "title = page.title()",
        "assert title == 'Playwright Library | Playwright'"
    ])

    # テンプレートに沿ってテストコードを作成する
    code = template.render(
        scenarios=[sd_1, sd_2, sd_3]
    )

    # テストコードを.pyファイルに書き出す
    with open(file="outputs/test_sample.py", mode="w") as pyfile:
        for line in code:
            pyfile.write(line.replace('\n', ''))


```

### 3. テストコードが生成される
下記のような形でpytestのコードが生成されます。
テンプレートで定義しておけば、docstringを動的に生成することも可能です。

```python

from playwright.sync_api import Page


class TestSample:

    def test_1_goto_main(self, page: Page):
        page.goto('https://playwright.dev/')
        title = page.title()
        assert title == 'Fast and reliable end-to-end testing for modern web apps | Playwright'

    def test_2_goto_doc_intro(self, page: Page):
        page.goto('https://playwright.dev/docs/intro')
        title = page.title()
        assert title == 'Installation | Playwright'

    def test_3_goto_doc_api(self, page: Page):
        page.goto('https://playwright.dev/docs/api/class-playwright')
        title = page.title()
        assert title == 'Playwright Library | Playwright'

```

# 所感
この技術だけで何か良い未来が見えるわけではないですが、テスト設計の自動化との相性の良さは感じました。
より良い方法がないか引き続き探っていきたいです！

# 参考
https://qiita.com/yasuaki_hiruta/items/fbe0ee55cf539cf99f64

https://github.com/pytest-dev/pytest-bdd