テスト用にJSON比較用のライブラリを作ってPyPIに登録してみた話

[JX通信社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/jxpress) 7日目の記事です。

## いきさつ

WebAPIのテストをするときに、リクエストに対して帰ってきたJSONをassertすることはよくあると思います。

PythonではJSONをそのまま扱うよりも辞書型にして扱うことが多いと思いますし、辞書型なら期待する値をリテラルで書いてレスポンスの値と比較できるので楽ですね。

しかし、例えばリソースの作成・更新日時などはテストの実行時に期待値を用意しても、assertの実行タイミングによって微妙な差が生じて単純に比較すると失敗してしまうことがあります。

ほかにも「ここの数値は期待値の±nまでは許したい」「このキーの値はUUID使うから無視したい」「Optionalだから要素があってもなくてもOK」など、いろんな基準でassertしたくなるケースがあると思います。

だからといって、assertしたい要素だけ取り出してから比較したり一部の要素だけ別の基準でassertしたりを個別にいちいち書くのも面倒です。

そこで、辞書型同士の比較の条件を簡単に指定できる[ライブラリ](https://github.com/TatchNicolas/chabie)を作ってみました。

作ったもの自体は非常に小さく(というか関数1つだけ)、簡単なものですがせっかくなのでPyPIに登録してみました。
ライブラリの使い方と、それをPyPIにアップロードしたとき手順をまとめてみます。

## 使い方

普通にpipでインストールします。Python3.6以上を想定しています。

```
pip install chabie
```

簡単なサンプルコードはこちら


```python
from chabie import chabie

chabie.compare_dicts({'foo': 100}, {'foo': 200}) # False
chabie.compare_dicts({'foo': 100}, {'foo': 100, 'bar': 200}) # False

# keysに{<キー>: <boolを返す関数オブジェクト>}を指定すると、特定のキーの比較の条件を指定できる
chabie.compare_dicts({'foo': 100}, {'foo': 105},keys={'foo': lambda a, b: abs(a-b) <= 5}) # True

# typesに{<クラスオブジェクト>: <boolを返す関数オブジェクト>}を指定すると、特定の型の比較の条件を指定できる
chabie.compare_dicts(
    {'foo': 'abcde'},
    {'foo': 'fghij'},
    types={str: lambda a, b: len(a)==len(b)}
) # True

# ignoreにリストで渡したキーは無視される
chabie.compare_dicts({'foo': 100}, {'foo': 100, 'bar': 200}, ignore=['bar']) # True
```

中身に辞書型やリストがネストされていても再帰的に比較します。
keysとtypesを同時に渡すとkeysが優先されます。


## PyPIへの登録

公式のドキュメントは[こちら](https://packaging.python.org/)

### PyPIでアカウント作成

[PyPIの登録フォーム](https://pypi.org/account/register/) からアカウントを作ります。
ユーザ名とパスワードが後でライブラリをアップロードする際に必要になります。

※ テスト用のPyPIとして、https://test.pypi.org/account/register/ もあります。
パッケージングやアップロードの操作に慣れたりCI/CDのテストしたい時に利用しても良いでしょう

### パッケージング

公式チュートリアルは[こちら](https://packaging.python.org/tutorials/packaging-projects/)

#### ディレクトリ構成

```
├── chabie
│   ├── chabie.py
│   ├── __init__.py
│   └── key.py
├── LICENSE
├── README.md
├── setup.py
```
#### setup.pyの作成

```setup.py
from setuptools import setup, find_packages


setup(
    name='chabie',  # pip installで使う名前を指定します
    version='0.3.0',  # バージョンを指定します
    description='Comparison utility', # どんなパッケージかを説明します
    author='TatchNicolas',  
    author_email='TatchNicolas@users.noreply.github.com',
    license='MIT',
    packages=find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ]
)
```

- `packages`: パッケージに含めるパッケージ(もっと上手い言い方したい)を指定します。
    - `find_packages()`を使うことでディレクトリ内で見つかるモジュールを良い感じに探してくれます。
- `classifiers`: ライセンスやサポートしている環境、プロジェクトの状態(alpha, betaなど)、用途などのメタ情報を指定します。

#### LICENSE、README.mdの作成
任意のライセンスと、適当なREADME.mdを作成します。
今回はMITライセンスにしてみました。

```:LICENSE
MIT License

Copyright (c) 2018 Tatch

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

#### setuptoolsでパッケージング

パッケージングに必要なパッケージをインストールします。

```
pip install setuptools wheel
```

続いてパッケージングします。

```
python3 setup.py sdist bdist_wheel
```
sdistは[Source Distribution](https://packaging.python.org/glossary/#term-source-distribution-or-sdist)、bdist_wheelは[Built Distribution](https://packaging.python.org/glossary/#term-built-distribution)のことです。
ここでは両方指定しておきましょう。

完了すると、プロジェクトフォルダに以下のようなファイルが作成されます。

```
├── chabie.egg-info
│   ├── dependency_links.txt
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   └── top_level.txt
└── dist
     ├── chabie-0.3.0-py3-none-any.whl
     └── chabie-0.3.0.tar.gz

```

### アップロード

いよいよアップロードです。`twine`コマンドを使います。

ユーザ名とパスワードは、以下のいずれかの方法で指定できます。

1. プロンプトから入力
2. [.pypirc](https://docs.python.org/3.6/distutils/packageindex.html#the-pypirc-file)というファイルに記載
3. コマンドラインのオプションで指定(`-u, --username` / `-p, --password`) 
4. 環境変数で指定

2.は間違えてGitHubへ大公開して大後悔しないようにご注意ください。
CI/CDにのせるなら、そのサービスの提供する方法で認証情報をストアすることになると思うので、3.か4.の方法が良いでしょう。

今回は環境変数を使ってみます。

```
$ export TWINE_USERNAME=<PyPIのユーザ名>
$ export TWINE_PASSWORD=<PyPIのパスワード>

# (注) test.pypi.orgを使う場合は、以下の環境変数でアップロード先を切り替えてください
$ export TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/
```

最後にアップロードコマンドを実行します。

```
$ twine upload  dist/*
```
めでたく自作のライブラリをPyPIにアップロードすることができました。

### ダウンロード

試しに`pip install`できるか試してみましょう。`test.pypi.org`を使った人は、URLの切り替えを忘れずに。

```
pip install  <アップロードしたリポジトリの名前>
pip install -i https://test.pypi.org/simple/ <アップロードしたリポジトリの名前> # test.pipy.orgの場合
```