pre-commitでテスト自動化

こんにちは、えいりんぐーです

みなさんはテストを書いていますか?
(ここでは単体テストを指しています)
また、テストを自動化していますか?

Circle CI や Travis CI などで自動テストを実施している方も多いかと思います
一方で、制限の厳しい状況 (クライアントや社内事情など) で仕事をしていると、容易には外部サービスを利用できない場合がありますね

今回は [pre-commit](https://pre-commit.com/) を使って git push 時にテストを自動で走らせる方法をご紹介します

# pre-commit

pre-commit は Python 製の [Git hook](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF) のラッパーです

自前でいちいちスクリプトを書くことなく、よくある処理 (リンタやフォーマッタなど) を実装することができます

フック自体には、コミット時やマージ時などの所定のタイミングをトリガーとすることができます

pre-push フックを使えば、プッシュ時にテストが自動で走るように設定できます

**余談**

pre-commit にはすでに[多くのフックがサポート](https://pre-commit.com/hooks.html)されていますが、pytest のフックは実装されていないようです

- https://github.com/pre-commit/pre-commit-hooks/issues/291
- https://stackoverflow.com/questions/64011304/running-pytest-as-a-pre-commit-hook-no-such-file-or-directory-issue

要望などは何度か上がっているようですが、コミット時に走らせるには、テストが遅いことがネックなようです
実際、コミットするたびにテストは過剰ですよね

# pre-push の設定

```.pre-commit.yaml
repos:
-   repo: local
    hooks:
    -   id: test
        name: test
        entry: pytest -v -s tests
        stages:
            - "push"
        language: system
        pass_filenames: false
        always_run: true
```

`stages` に `"push"` を指定すると、プッシュ時にフックを実行します

`entry` に実行するコマンドを設定します

`pre-commit install -t pre-push` で pre-push フックをインストールします

# コード例

ディレクトリ構成

```
+ .pre-commit-config.yaml
+ mypackage
|  + __init__.py
|  + foo.py
|
+ tests
|  + __init__.py
|  + test_foo.py
|
+ requirements.txt
```

```foo.py
def foo():
    return "bar"
```

```test_foo.py
from mypackage import foo


def test_foo():
    assert foo.foo() == "foo"
```

このコードをプッシュしようとするとテストが走り、失敗するため、プッシュにも失敗します

```
$ git push origin pre-commit
test.....................................................................Failed
- hook id: test
- exit code: 1

============================= test session starts =============================
platform win32 -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0 -- C:\Users\***\Programs\mypackage\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\***\Programs\mypackage
collecting ... collected 1 item

tests/test_foo.py::test_foo FAILED

================================== FAILURES ===================================
__________________________________ test_foo ___________________________________

    def test_foo():
>       assert foo.foo() == "foo"
E       AssertionError: assert 'bar' == 'foo'
E         - foo
E         + bar

tests\test_foo.py:5: AssertionError
=========================== short test summary info ===========================
FAILED tests/test_foo.py::test_foo - AssertionError: assert 'bar' == 'foo'
============================== 1 failed in 0.09s ==============================
```

`foo` 関数の返り値をテストが通るように修正してプッシュします

テストを通過するのでプッシュが成功します

```
$  git push origin pre-commit
test.....................................................................PassedEnumerating objects: 26, done.
Counting objects: 100% (26/26), done.
Delta compression using up to 8 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (25/25), 2.12 KiB | 1.06 MiB/s, done.
Total 25 (delta 9), reused 2 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (9/9), done.
remote:
remote: Create a pull request for 'pre-commit' on GitHub by visiting:
remote:      https://github.com/***/mypackage/pull/new/pre-commit
remote:
To github.com:***/mypackage
 * [new branch]      pre-commit -> pre-commit
```

テストが成功するまでプッシュできないので、テストを回避しない限りは、ある意味テスト駆動開発が強制されることになります笑

# 終わり

如何だったでしょうか

テストを実行するコマンドをいちいち実行しなくても、プッシュするだけでテストが走るので便利ですね
また、Circle CIなどでテストを実行する場合と違い、ローカルで実行するため、レスポンスを早く得られるというメリットもあります

pre-commit には他のタイミングや処理のフックもあるので、ぜひご活用ください
本記事がみなさんの参考になれば幸いです