VSCode での Python 開発環境ことはじめ(Chromebook版)

# 概要
 * VSCode で新規に Python 開発するときに
 * せっかくなので Chromebook で試してみます

# はじめに
Python は気軽に手を出せて、かつ実用性も申し分ないのでお気に入りですが、大規模開発に耐えるプロジェクト設計やテストコード開発のやり方については悩ましい部分もあるのではないでしょうか。
ということで、自分的定番構成を紹介してみたいと思います。

## Python のアピールポイント
numpy 使って大規模な数値演算をするわけでもなく、AWS Lambda の軽量コードを書くくらいの私ですが、下記のようなメリットを感じて利用しております。

 * 学習コストが低い
    - つまり人材採用コストが低い: プログラマー目線だとデメリットにしか映らないでしょうが管理目線だとよいでしょ？
    - (ただし深淵を極めるとそれなりに深いので、難しいのが好きなプログラマーさんも悪くないのでは？)
 * 環境依存しづらい
    - インタプリタ言語だから OS に依存したりしない
        - 例えば Win/Mac の派閥が割れているとして、混合編成でもかなり行ける
        - なんなら 今回は Chromebook で開発
        - もっと言うと、paiza とかならブラウザでお試し実行できる
    - 2.7 vs 3.x 互換問題とか既に過去のもの
        - 2.7 を新規採用しなければいいので、少なくとも今後の開発には関係ない
 * ユニットテストもだいぶ書きやすくなった(個人比)
    - コードカバレッジ調査や性能計測もお手のもの
    - (このあたりは過去の失敗からノウハウを得た部分が大きい)
 * VisualStudio Code と相性がいい
    - Microsoft 純正の Python エクステンションが大抵のことはやってくれる

# 環境構築
Windows/Mac でも構わないのですが、せっかくなので (流行り始めの？)Chromebook を使ってみます。
Chromebook の環境構築については参考文献がたくさんあるので、参考にして構築します。

https://tech-camp.in/note/pickup/101453/


## Linux 有効化
Chromebook 上では Debian 10 ベースの Linux を起動することができ、開発環境は基本的に Linux 上へ構築します。
さっそく有効化しましょう。

|||
|:--|:-:|
|設定画面を開きます。|![InkedScreenshot 2021-10-10 10.32.09_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/f6199203-81f1-777f-1851-2091a418c572.jpeg)|
|デベロッパー > Linux 開発環境 から有効化します。　(今回は有効化済み)|![InkedScreenshot 2021-10-10 10.33.05_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/20001342-1aba-e541-9df3-86ca14b67b49.jpeg)|
|(有効化すると、Linuxアプリがメニューに現れます)|![Screenshot 2021-10-10 10.35.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/5ec8a7a2-18d3-ff92-6297-1eb754b7a522.png)|

続いて、Linux 環境に VSCode を導入します。
(先ほどもう入っていましたが、入れ方のおさらい)

https://code.visualstudio.com/download

公式に沿って導入すれば大丈夫です。

https://code.visualstudio.com/blogs/2020/12/03/chromebook-get-started

|||
|:--|:-:|
|Debian なので .deb を選びましょう。|![InkedScreenshot 2021-10-10 10.36.31_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/0460ed83-e1a6-dd45-e46f-84d139d75577.jpeg)|

導入後は、拡張機能から日本語パックを探し、日本語化しておきます。
(まあ英語のままでも全然大丈夫そうですが)

![InkedScreenshot 2021-10-10 10.46.45_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/ac01d089-5358-0e27-f50b-e81e32ade424.jpeg)


続いて、Microsoft 公式の Python 拡張機能も導入しておきます。

![Screenshot 2021-10-10 10.48.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/4bdd4bce-af7b-72d3-774c-a23964b45e2c.png)


これでひとまず最初の設定は完了です。


## ディレクトリ構成
VSCode を Linux アプリとして導入したので、Chromebook 側における「Linux ファイル」の中が見える仕組みです。
このディレクトリに実際のプロジェクトファイルを作っていきましょう。

![Screenshot 2021-10-10 10.50.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/a6527368-14e2-262b-9f90-c25900add248.png)


# プロジェクト構築
この先は、Chromebook 以外の OS でも共通で使えます。
(※ VSCode なら)

## ディレクトリ構成
新規の Python プロジェクトを作るとして、ひとまず下記のようなテンプレートでディレクトリを切ると快適です。

```
<project_folder>
 ┗ src : メインアプリケーションの プロジェクトルート (デプロイ時にはこのディレクトリだけアップします)
     ┗ application : アプリ本体用
     ┗ common : 共通ロジック(エラー処理とか)用
     ┗ service : (DBなどのリソースアクセス含む)処理サービス用
     ┗ (※この辺はプロジェクトの規模や、チームが信じる設計思想によって自由に足してよいです)
     ┗ __init__.py : Python プロジェクトのモジュール認識ファイル(各ディレクトリに必要. モジュールの初期化処理など書くこともできるが、空でも可。)
     ┗ requirements.txt : メインソース用のライブラリ requirements。デプロイコード(AWS SAM とか)に読ませる用です

 ┗ tests : pytest テストファイル
     ┗ (※次章へ)

 ┗ .vscode : VS Code 設定ファイル
     ┗ settings.json

 ┗ pytest.ini :  pytestの設定ファイル
 ┗ .env :  VS Code に src の プロジェクトルートを伝える(開発環境用)
 ┗ .gitignore :  作業ディレクトリを git から除外する
 ┗ README.md : 取り扱い説明
 ┗ requirements.txt :  開発環境用の requirements (テスト用ライブラリなど入れてよい)
```

### テストコードディレクトリ
Python 用のテストスイートはいろいろありますが、今回は pytest を使用します。
自由度と手軽さのトレードオフを考えて、バランスの良さから選定しています。

```
<project_folder>
 ┗ tests : pytest テストファイル
    ┗ __init__.py : Python プロジェクトのモジュール認識ファイル(functions と同様に各ディレクトリへ配置する)
    ┗ conftest.py : テスト全体の初期処理を定義するファイル。ここでメインソースに path を通す。
    ┗ application
        ┗ __init__.py
        ┗ conftest.py : テストの前後処理(fixture) を定義するファイル。(同一ディレクトリ内の全ファイルで共用する。)
        ┗ test_xxx.py : テストファイル本体(pytest に認識させるため、ファイルの接頭辞は「test」である必要がある。原則として src の 1ファイルに付き1つ作成する。)
        ┗ test_yyy.py
    ┗ common
        ┗ __init__.py
        ┗ conftest.py
        ┗ test_xxx.py
        ┗ test_yyy.py
    ┗ service
        ┗ __init__.py
        ┗ conftest.py
        ┗ test_xxx.py
        ┗ test_yyy.py
    (※もちろん、src のディレクトリ構成に合わせて好きに作ってください)
```


## 設定ファイル
VSCode 上で一通りの作業が行えるよう、設定をチューンしていきます。

### VSCode の設定
settings.json に Pytest 拡張を有効化するための設定を書いておきます。
Microsoft 公式の Python Extension が、テストを管理してくれるようになります。

```json:settings.json
{
    "python.testing.pytestEnabled": true
}
```

### プロジェクト設定
メインソースのルートを src ディレクトリに設定します。
ルートディレクトリではなく、あえて src に変えると、

 * メリット
    * テストコードや開発用の余計なファイルを、デプロイ時にアップロード対象から除外できる
    * 開発環境用と メインソース用で requirements.txt を別にできる
 * デメリット
    * 放っておくと import とかが上手くいかない

というところで、本番ソースが汚れにくいのがうれしいですが、import 周りのセットなど工夫が要ります。

まず、メインソースは 「.env」 内で定義するだけで切り替わります。

```json:.env
PYTHONPATH=src
```

これだけだと pytest がインポート失敗で落ちるので、tests の conftest に path を追加します。

```py:tests/conftest.py
import sys 
import os

sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src/"))
```

この設定ののち、メインソース上での import は src 配下のパスだけ書くようにします。
要は src を書きません。

```py:src/sample.py
from service.hoge import HogeService
from common.huga import HugaException
```

テストコード上での import は、ルートディレクトリからのフルパスを書くようにします。
要は src を入れます。

```py:tests/test_sample.py
from src.service.hoge import HogeService
from src.common.huga import HugaException
```

### テスト設定
pytest の設定で、性能計測やカバレッジレポートを有効化しておきます。
そんなリッチなテストが不要な場合は設定も不要です。

```ini:pytest.ini
[pytest]
testpaths = .
python_files = test_*.py
python_classes = Test
python_functions = test_
addopts = --durations=0 --cov=src/ --cov-report=html
```

ちなみに、設定で XUnit 形式のレポートを選ぶと、Jenkins などに連動することもできるんですって。
私は使ったことないのですが、可能性を感じられますね。

http://kuma8.hatenablog.jp/entry/20121124/1353723982

### 開発環境向け
開発用の requirements として、例えばこんな設定を書きます。

```txt:requirements.txt
pytest
pytest-cov
```

 * pytest, pytest-cov は、テストコードを使うにあたって必須です。


gitignore も設定しておきます。

```:.gitignore
*.pyc
.coverage
htmlcov/
```

 * htmlcov にはカバレッジレポートが出るので、エビデンスとして git commit したい場合は外してもよいです。

## テストコード
pytest のテストコード基本設計です。
(細かい説明をバッサリ省略しますが、詳細は別記事が書けたらいいな)

### メイン処理

```py:test_hoge.py
import os
import pytest

from src.service.hoge_service import HogeService


# Test で始まるクラス名を命名すると、テストとして認識される。(※ただしクラス定義は必須ではない)
## conftest.py で定義した fixture(事前/事後処理)のうち、使用するものを usefixtures として宣言する。
## fixture のうち、mock_environ などは関数から呼び出していないけれど、ここに書いておく。(テスト開始時に読み込んで、モックなどに必要な環境変数を初期化できる)
@pytest.mark.usefixtures(
    'mock_environ', 'create_mock', 'get_hoge_service'
)
class TestHogeService:
    ### 関数に入れたいテストパラメータのパターンを parametrize として宣言する
    @pytest.mark.parametrize(
        'subject, request_list, is_valid',
        [
            ('通常', [{'aaa': 'bbb'}, {'ccc': 'ddd'}], True),
            ('リクエスト対象が空', [], True),
            ('リクエスト対象がNone', None, False),
        ]
    )
    ### test で始まる関数名を命名すると、テストとして認識される。 (※ Test で始まるクラス内、もしくはクラス未所属が対象。)
    #### 引数に必要なもの
    #### 1.self (クラスの配下である場合)
    #### 2.parametrize で定義した引数群
    #### 3.使用する fixture
    def test_execute_requests(self, subject, request_list, is_valid, monkeypatch, get_hoge_service, create_mock):
        with mock():
            #### fixture は関数内で普通に呼び出せる。(この例では create_mock が fixture)
            with create_mock('dammy') as mock_resource:
                with get_hoge_service() as service:
                    ### monkeypatch という fixture は pytest に標準搭載される。
                    ### 指定の関数を丸ごと別のダミー関数へ差し替えてくれる。
                    monkeypatch.setattr(HogeService, 'execute', lambda *args, **kwargs: None)
                    if is_valid:
                        result = service.execute_requests(subject, request_list)

                        ### assert 判定文, エラーの場合表示する文字列   と書くとテスト項目になる。
                        assert result == None, '関数が何か不要な値を返している'
                    else:
                        ### 例外が発生すること が期待値のときは assert の代わりに raises を使用する。
                        with pytest.raises(Exception, match=r".*execute_requests.*") as exception_info:
                            service.execute_requests(subject, request_list)

                        ### 発生した例外を受け取って細かくチェックすることも可能
                        raised_exception = exception_info.value
                        if raised_exception.__cause__:
                            assert isinstance(raised_exception.__cause__, Exception), 'エラーの発生元がハンドリングできていない'
                        else:
                            assert False, '@error_handle し忘れているかもしれない (raise from されていない)'
```

 * メインソースの1ファイルにつき1つの test ファイルを作る。
 * 1つのクラスにつき、1つの test クラスを作る。
 * 1つのメソッドにつき、1つの test メソッドを作る。
 * テストパターンは原則として parametrize で制御する。
 * parametrize でどうしても書きづらいパターン(途中でエラー発生するパターン等)のみ、別のメソッドで表現する。

と、いい感じにテストコードが書ける気がします。



### 事前/事後処理(fixture)

```py:conftest.py
import pytest

import os
import contextlib
from src.common.exception import FatalError
from src.service.hoge_service import HogeService

# @pytest.fixture デコレータを定義すると、fixture として認識される。
# scope の指定内容によって、中身が実行される回数が変わる。
#    function	テストケースごと 1回 (毎回初期化したいダミーDB作成処理など)
#       class	テストクラス生成ごと 1回
#      module	テストファイルの読み込みごと 1回
#     session	テスト実行ごと 1回 (毎回固定値を返せばいい場合など)
# 実行される回数に関係なく、return された値はテストケースのどこでも使用できる。

# 何も値を返さない(テストロード時に実行されるだけ) の例
# 念のために function スコープとして各テストケースごと初期化する
@pytest.fixture(scope='function')
def mock_environ():
    os.environ['Region'] = 'ap-northeast-1'

# 単純な固定値を返す 例
# 固定の値だから session でよい
@pytest.fixture(scope='session')
def get_dammy_data():
    return 'hoge'

# 関数を返す例
# 固定の関数を返すから session でよい。(返した関数を実行するタイミングはテストケース内でコントロール)
# ※contextmanager や yield は with 句として呼びたい目的の実装なので pytest の仕様とは関係ないです。
@pytest.fixture(scope='session')
def get_hoge_service():
    # with 句で行う処理
    @contextlib.contextmanager
    def _create_service(hoge):
        yield HogeService(input=hoge)
    return _create_service

# ※例外を発生させるダミー関数を返したりもできる
@pytest.fixture(scope='session')
def dammy_fatal_error():
    # with 句で行う処理
    @contextlib.contextmanager
    def _dammy_function():
        def _fatal_error(*args, **kwargs):
            raise FatalError('Mock')
        yield _fatal_error
    return _dammy_function

# 何らかの処理 + 値の組み合わせを返す例
## モックリソースを新規作成 + 作ったリソースの参照を返す
@pytest.fixture(scope='session')
def create_mock():
    # with 句で行う処理
    @contextlib.contextmanager
    def _create(name):
        result = create_hoge(Name=name)

        yield result
    return _create
```


# 実践
簡単にサンプルを作って動かしてみます。
まず最初に、ルートディレクトリで requirements のインストールを行っておきましょう。

```bash
pip3 install -r requirements.txt
```

そして、適当にアプリを作ってみます。


|(こんなアプリ作ってみた)|
|:--:|
|![Screenshot 2021-10-10 15.00.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/a41c5d70-5eea-4659-8d01-4ecb8ed52f17.png)|
|![Screenshot 2021-10-10 15.02.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/be6da46c-b8be-d927-d418-2e2fe1ac9fdd.png)|

|(簡単なテストコードも書いてみた)|
|:--:|
|![Screenshot 2021-10-10 15.11.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/a70ccbb3-2f2e-9e35-0d43-b653019514f6.png)|


テストコードを VSCode 上で実行してみましょう。

|||
|:--|:--:|
|テストコードタブを選び、実行を選択します。|![InkedScreenshot 2021-10-10 15.22.05_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/fd9500fe-cb72-bfd5-9f92-8b7e1e36ae73.jpeg)|
|動きました|![Screenshot 2021-10-10 15.22.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/5b528239-87dc-7a12-def0-185ab2720cc4.png)|


|こんな感じでカバレッジレポートが出ます。|
|:--:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/a60072a8-ae6b-80d0-d2a5-2166c53db05f.png)|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/ffcbca0a-bab2-9c9f-d9c6-a5a5bbae6524.png)|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/d80167f8-50de-b26c-9341-e6dc30593df8.png)|

|メインコードのほうに新しいメソッドを足してみると、カバレッジ missing 判定されましたね。|
|:--:|
|![Screenshot 2021-10-10 15.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/cae2e138-1cc4-a79e-cf40-489fb6f4a12d.png)|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/a91c3f61-ca9f-ab6e-2084-3168e18be142.png)|

もう少し複雑なテストコードも組んでみます。


|||
|:--|:--:|
|書いてみた|![Screenshot 2021-10-10 15.30.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/32e05ea0-1da1-1e4f-3ed1-257f09e5bfda.png)|
|テスト用のクラスを fixture で作ってみます。ロジックを複数のコードで使いまわせるようになります。|![Screenshot 2021-10-10 15.31.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/987c684c-2246-5f82-bc9f-508d61ad1e49.png)|
|parametrize で、同じテストコードを複数の入力パターンで試せます。|![InkedScreenshot 2021-10-10 15.34.01_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/9ed09b4f-8f82-4c97-3d44-6dcf0611c8d7.jpeg)|

ちなみに、pytest が標準出力に出してくれる表示項目も確認可能です。

|||
|:--|:--:|
|ターミナル出力で「Python Test Log」を選びます。|![InkedScreenshot 2021-10-10 15.35.22_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/cf1883a1-7c9f-a909-fc15-564912a7b936.jpeg)|
|実行時間のチェック結果などがこちらへ出ています。|![InkedScreenshot 2021-10-10 15.35.10_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222587/27f9495a-2e9e-0b21-eca8-45fa6dc4a588.jpeg)|


# おわりに
最初のひと手間を掛ければ、かなり快適な Python 開発ができるかもしれません。