競プロ用にテストを書く(VSCode + Python + pytest)

Pythonで競プロをやってみて、せっかくならテストを書いてみようと思いました。


## 環境
 
- Python: 3.7.5
- pytest: 5.3.2
- VSCode: 1.41.1


## ディレクトリ構成

ファイルの置き場所等を先に示しておきます。

```
.
├── .vscode
│   └── settings.json
├── abc086
│   ├── __init__.py
│   ├── a.py
│   └── test_a.py
├── conftest.py
└── pytest.ini
```

## VSCodeの設定

`.vscode/settings.json` にVSCode側の設定を書き込みます。

```..vscode/settings.json
{
  "python.pythonPath": "${workspaceFolder}/.venv/bin/python3",
  "python.testing.pytestArgs": [],
  "python.testing.pytestEnabled": true
}
```

## pytestの設定

ルートディレクトリとなる場所に 
- `pytest.ini`
- `conftest.py`
を設置します。

`pytest.ini` は好みの設定をしてください。

```pytest.ini
[pytest]
addopts = -v
junit_family=legacy
```

入力と出力をチェックする関数を `conftest.py` に書きました。

```conftest.py
import re
import pytest

reg_ss = re.compile(r'^\s+', re.MULTILINE)
reg_nl = re.compile(r'\n')
reg_ex_nl = re.compile(r'^\n')


@pytest.fixture
def check_result(capsys):
    def _check_result(app, func, inp, ans):
        def preprocess(text):
            regs = (reg_ex_nl, reg_ss)
            for r in regs:
                text = r.sub('', text)
            return text

        inp_list = reg_nl.split(preprocess(inp))
        ans = preprocess(ans)
        app.input = lambda: inp_list.pop(0)
        func()

        out, err = capsys.readouterr()
        assert out == ans
        assert err == ''

    return _check_result

```



`check_result` がヘルパー関数です。ちなみに、 `capsys` は出力を扱うpytestのfixtureです。（[Capturing of the stdout/stderr output — pytest documentation](https://docs.pytest.org/en/latest/capture.html)）

## 実際のテスト

**AtCoderのABC過去問題の86のAを引用します。**

リンク: [A - Product](https://atcoder.jp/contests/abc086/tasks/abc086_a)

### 回答コード
```a.py
# -*- coding: utf-8 -*-


def main():
    a, b = map(int, input().split())
    print('Odd' if (a * b) % 2 else 'Even')


def wrong():
    a, b = map(int, input().split())
    print('Even' if (a * b) % 2 else 'Odd')


if __name__ == "__main__":
    main()

```

※ `wrong()` はこの記事用の関数で、誤答を出力する関数なので、通常は書かないです。


### テストコード
```test_a.py
import abc086.a as app


def test_1(check_result):
    inp = '''
    3 4
    '''
    ans = '''
    Even
    '''

    check_result(app, app.main, inp, ans)


def test_2(check_result):
    inp = '''
1 21
    '''
    ans = '''
Odd
    '''

    check_result(app=app, func=app.wrong, inp=inp, ans=ans)
```

`test_2` のように、入力例や回答例のコピペが違ってもある程度は、ヘルパー関数側で吸収するようにしました。

### テスト結果の出力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110426/b0142a83-fe5e-ced1-8677-138a318efdb1.png)

```
python /Users/john/.vscode/extensions/ms-python.python-2019.11.50794/pythonFiles/testing_tools/run_adapter.py discover pytest -- --rootdir /Users/john/dev/atcoder/ABC -s --cache-clear
============================= test session starts ==============================
platform darwin -- Python 3.7.5, pytest-5.3.2, py-1.8.0, pluggy-0.13.1 -- /Users/john/dev/atcoder/ABC/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/john/dev/atcoder/ABC, inifile: pytest.ini
collecting ... collected 2 items

abc086/test_a.py::test_1 PASSED                                          [ 50%]
abc086/test_a.py::test_2 FAILED                                          [100%]

=================================== FAILURES ===================================
____________________________________ test_2 ____________________________________

check_result = <function check_result.<locals>._check_result at 0x10d8e3170>

    def test_2(check_result):
        inp = '''
    1 21
        '''
        ans = '''
    Odd
        '''
    
>       check_result(app=app, func=app.wrong, inp=inp, ans=ans)

abc086/test_a.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

app = <module 'abc086.a' from '/Users/john/dev/atcoder/ABC/abc086/a.py'>
func = <function wrong at 0x10d8e37a0>, inp = '\n1 21\n    ', ans = 'Odd\n'

    def _check_result(app, func, inp, ans):
        def preprocess(text):
            regs = (reg_ex_nl, reg_ss)
            for r in regs:
                text = r.sub('', text)
            return text
    
        inp_list = reg_nl.split(preprocess(inp))
        ans = preprocess(ans)
        app.input = lambda: inp_list.pop(0)
        func()
    
        out, err = capsys.readouterr()
>       assert out == ans
E       AssertionError: assert 'Even\n' == 'Odd\n'
E         - Even
E         + Odd

conftest.py:24: AssertionError
- generated xml file: /var/folders/_p/blhkz_jj069cj8_zgmhd3dd40000gn/T/tmp-9833OeiIvOjzde9.xml -
========================= 1 failed, 1 passed in 0.05s ==========================
```

`test_1` は正常に通りました。
`test_2` は期待する回答と違うので、`AssertionError` が出ています。



## 参考文献等

- [Mocking input and output for Python testing](https://code-maven.com/mocking-input-and-output-for-python-testing)
    - ヘルパー関数の`main.input` 周りを参考にしました。
- [pytest ヘビー🐍ユーザーへの第一歩 - エムスリーテックブログ](https://www.m3tech.blog/entry/pytest-summary)
    - 結果の出力が `AssertionError` しか出ていないと思って、モジュールからヘルパー関数に書き換えたときの参考にしました。