Tavernを使ってみよう！

今年初めて投稿したので初投稿です。
この記事を読んでいるということは、Tavernに興味を持ったということでしょう。
私もちょっとした機会でTavernを使うことになり、記事を読み漁りました。
今回は、「そのTavernとは何か。」から始まりTavernの使い方について詳しく話していこうと思います。

# Tavernって何？
公式はここから。https://tavern.readthedocs.io/en/latest/#

Tavernとは、「APIから欲しい情報が正常に取れているかどうかを確認するテストになります。」
なのでTavernを使い所さんは、単体テストやシステムテストなどのテスト関係で使用します。
利点としては
- 導入が楽
- APIのテストをコードで書ける
- コスト0
- 学習コスト低

こんな所ですかね
ちなみに使用する言語は、Pythonです

# 使ってみよう

Tavernを早速インストールしてみましょう。
下記のコマンドでインストールすることができます。

```
$ pip3 install tavern
```

# APIテストを書いてみよう

tavernでのテストコードは、yamlファイルで書きます。

yamlファイルの名前は、

```
test_*.tavern.yaml　(* の所はなんでも良い)
```

にしましょう。

ファイル名をこうすることで、実行時にtavern側が勝手に「このファイルはテストファイルだな。」と判断してくれます。

# テストを書いてみよう。

テストコードを書いてみましょう。簡単なコードを書いてみようと思います。

```test_sample.tavern.yaml
test_name: 簡単なテスト

stages:
  - name: 200が返ってくるかどうかのテスト
    request:
      url: https://jsonplaceholder.typicode.com/posts/1
      method: GET
    response:
      status_code: 200
```

# 実行してみよう

下記のコードで実行することができます。

```
$ pytest test_sample.tavern.yaml
```

また、 -v オプションをつけることでエラーが起きた際により詳しく情報を表示してくれます。

```
$ pytest test_sample.tavern.yaml -v
```

pytestだけでも実行可能です。この場合、「test_*.tavern.yaml」この名前に該当するyamlファイル全てを実行します。

```
$ pytest
```

いろいろターミナルに表示されますが、大事なのはここです。

```
$ pytest test_sample.tavern.yaml
====================================== test session starts =======================================
platform darwin -- Python 3.8.1, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Volumes/Transcend/tavern
plugins: tavern-1.11.1
collected 1 item                                                                                 

↓ここ
test_sample.tavern.yaml .                                                                  [100%]

======================================== warnings summary ========================================
...
================================= 1 passed, 2 warnings in 3.94s ==================================
```

```
test_sample.tavern.yaml .                                                                  [100%]
```

これが、テスト結果を表示しています。テストがうまくいくとうまく行った箇所を「.」で表し、最後に passedと表示されます。逆にテストがうまく行ってないと

```
$ pytest test_sample.tavern.yaml
====================================== test session starts =======================================
platform darwin -- Python 3.8.1, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Volumes/Transcend/tavern
plugins: tavern-1.11.1
collected 1 item                                                                                 

test_sample.tavern.yaml F                                                                  [100%]

============================================ FAILURES ============================================
```

このようになり、成功の場合「.」になっていた場所が「F」となり、FAILURESとつきます。

# 実際にコードを書いてみよう

さて、導入から実行まで一通り行うことができたので、次はテストコードについて詳しく書いていきましょう。

Tavernは基本的にAPIを叩いたらJson形式でくることを想定して作られています。

## しっかり想定しているJsonが取得できるか確認したい

このAPIで試してみましょう。
https://jsonplaceholder.typicode.com/posts/1
これを叩くと、このようなレスポンスがきます。

```
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
```

これをテストで書くとこうなります。

```yml
test_name: 簡単なテスト

stages:
  - name: 想定されたjsonが返ってくるかどうかのテスト
    request:
      url: https://jsonplaceholder.typicode.com/posts/1
      method: GET
    response:
      status_code: 200
      ↓ここでjsonの中身を判定してます
      json:
        userId: 1
        id: 1
        title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
        body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"

```

## テスト項目を複数書きたい

1ファイルに1項目？　違います。複数テストを書くことももちろん可能です。
「stages:」 の後のnameから複数作ることが可能です。

一応、「stages:」を複数書いてテストもかけますが、- nameからかけば、テスト項目を書けるので、-nameから書くことをお勧めします。

```yml
test_name: 簡単なテスト

stages:
  - name: ID=1に該当するJsonデータを取得
    request:
      url: https://jsonplaceholder.typicode.com/posts/1
      method: GET
    response:
      status_code: 200
      json:
        userId: 1
        id: 1
        title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
        body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"

  - name: ID=2に該当するJsonデータを取得
    request:
      url: https://jsonplaceholder.typicode.com/posts/2
      method: GET
    response:
      status_code: 200
      json:
        userId: 1
        id: 2
        title: "qui est esse"
        body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```

## headerのテストをしたい。

header情報をテストすることもできます。

```yaml
test_name: 簡単なテスト

stages:
  - name: ID=1に該当するJsonデータを取得
    request:
      url: https://jsonplaceholder.typicode.com/posts/1
      method: GET
    response:
      status_code: 200
      headers:
        content-type: application/json; charset=utf-8
      json:
        userId: 1
        id: 1
        title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
        body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
```

## テスト項目書いたけどまだテストしたくない!

skip を使いましょう。
skipをつけた項目はテストされずにスキップされます。

```yml
test_name: 簡単なテスト

stages:
  - name: ID=1に該当するJsonデータを取得
    skip: true  ←ここを追加したよ！
    request:
      url: https://jsonplaceholder.typicode.com/posts/1
      method: GET
    response:
      status_code: 200
      headers:
        content-type: application/json; charset=utf-8
      json:
        userId: 1
        id: 1
        title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
        body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
```

## json以外の項目もテストしたい!(外部関数)

### 外部関数の使い方
TavernはJsonをメインにテストを行うように作られています。なので、画像や動画などのJson以外のデータを扱う場合、pythonで書いたテスト関数をtavernで呼ぶようにします。

例えば、上でテストしたように。

```yml
- name: ID=2に該当するJsonデータを取得
    request:
      url: https://jsonplaceholder.typicode.com/posts/2
      method: GET
    response:
      status_code: 200
      json:
        userId: 1
        id: 2
        title: "qui est esse"
        body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```

これを外部関数を呼んで、同じようにテストしてみましょう。

外部関数を呼ぶ時にディレクトリ構造は大事になってくるので、ここでディレクトリ構造を記載しておきます。

```
tavern
  └ helper
    └ testing_util.py
  └ test
    └ __init__.py
    └ test_*.tavern.yaml
```

yamlファイルの中身はこうなります。

```test_*.tavern.yaml

  - name: 指定した画像かテスト
    request:
      url: https://jsonplaceholder.typicode.com/posts/2
      method: GET
    response:
      status_code: 200
      ↓ ここで外部関数を呼んでます
      verify_response_with:
        function: helper.testing_utils:is_specified_json
```

外部関数はこのようになります。

```helper/testing_util.py
def is_specified_json(response):
  json = response.json()
  assert json.get("userId") == 1  
  assert json.get("id") == 2
  assert json.get("title") == "qui est esse"
  assert json.get("body" ) == "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```

このようにして、外部関数を扱うことができます。しかし、関数の中で値を決めてしまっては汎用的な関数になりません。なので、比べる値を関数の引数に渡せるようにしましょう。

```test_*.tavern.yaml
  - name: 指定した画像かテスト
    request:
      url: https://jsonplaceholder.typicode.com/posts/2
      method: GET
    response:
      status_code: 200
      verify_response_with:
        function: helper.testing_utils:is_specified_json
        ↓ ここ追加
        extra_kwargs:
          schema: 
            userId: 1
            id: 2
            title: "qui est esse"
            body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```

外部関数を変えましょう。

```helper/testing_util.py
def is_specified_json(response, schema):
  json = response.json()
  assert json.get("userId") == schema.get("userId")
  assert json.get("id") == schema.get("id")
  assert json.get("title") == schema.get("title")
  assert json.get("body" ) == schema.get("body")
```

こうすることで、テスト項目を追加しているtavern側で値を指定することができました。

### 画像を取得するAPIをテストしたい

このAPIをテストしてみましょう。https://http.cat/404

![404.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293066/61458aab-c88e-3155-a781-12c7fc2f776b.jpeg)
このような可愛い猫の画像が取得できるAPIです。

これを外部関数を使ってテストします。

```helper/testing_util.py
# 取得した画像が正しいかテストする関数
def is_specified_image(response, schema):
  img = Image.open('img/' + schema.get("title"))
  img_byte_arr = io.BytesIO()
  img.save(img_byte_arr, format='jpeg')
  img_byte_arr = img_byte_arr.getvalue()

  i = Image.open(BytesIO(response.content))
  i_byte_arr = io.BytesIO()
  i.save(i_byte_arr, format='jpeg')
  i_byte_arr = i_byte_arr.getvalue()

  assert img_byte_arr == i_byte_arr
```

他にもやり方あると思いますが、こんな感じにしてみました。
あとは、これをyaml側で呼ぶだけです。

```test_*.tavern.yaml
  - name: 指定した画像かテスト
    request:
      url: https://http.cat/404
      method: GET
    response:
      status_code: 200
      verify_response_with:
        function: helper.testing_utils:is_specified_image
        extra_kwargs:
          schema: 
            title: '404.jpeg'
```

### ModuleNotFoundで怒られる

私はこの外部関数を呼ぶにあたってModuleNotFoundで結構怒られました。
Pythonにそんなに慣れてなく起きたエラーです。
その備忘録もかねてここに残しておきます。

#### 起きる原因

ディレクトリ構造を

```
tavern
  └ helper
    └ testing_util.py
  └ test
    └ __init__.py
    └ test_*.tavern.yaml
```
このようにしたことにより、yamlで宣言している関数がどこにあるかわからなくなってしまっている状態です。
Tavernのgithubには、同じディレクトリ内にyamlファイルと外部関数を置いていたので、そのようなことはなかったのですが、プロジェクトにおいてディレクトリ構造を理解できるような形にすることはとても大事なことです。なので、この現象はTavernを使ったプロジェクトを行う際は絶対に起こる問題だと思います。

この問題を解消するのが、 `__init__.py` です。
yamlで宣言している関数がどこにあるかわからなくなる問題として、外部関数をyaml内で宣言した場合、Tavern側に直接用意されている関数を見にいきます。それだと自分の用意した関数はそこにないので「ないよ」とエラーが起きてしまっています。

なので、`__init__.py`をtestを行うyamlと同じディレクトリ内に置きます。
そうすることで、python側で、`__init__.py`の一つ上のディレクトリ(例でいう、tavernディレクトリ)から見てくれるようになります。

あとは、yaml内の関数宣言を、このようにhelperから覗けるようにするだけです。

```test_*.tavern.yaml
  - name: 指定した画像かテスト
    request:
      url: https://http.cat/404
      method: GET
    response:
      status_code: 200
      verify_response_with:
        # ↓tavernディレクトリから見れるのでhelperから書く
        function: helper.testing_utils:is_specified_image
        extra_kwargs:
          schema: 
            title: '404.jpeg'
```
なお、/ ではなく . で書くのにご注意ください。


## 同じ変数を宣言するのがめんどくさい場合

tavernでは、外部ファイルを使うことによって、変数を別のファイルに持っておくことができます。
見てみましょう。

```test_*.tavern.yaml
  - name: ID=2に該当するJsonデータを取得
    request:
                   #↓ここ注目
      url: https://www.server-dev.com/posts/2
      method: GET
    response:
      status_code: 200
      json:
        userId: 1
        id: 2
        title: "qui est esse"
        body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```
例えば、このようなurlを叩くテストを書いたとしましょう。
これを変数をおいておくファイルを用意することでこのように分けることができます。

```common.yaml
name: サーバー情報
description: 環境

variables:
  host: www.server-dev.com
```

```test_*.tavern.yaml
test_name: 簡単なテスト

# ↓ここ追加
includes:
  - !include common.yaml

stages:
  - name: ID=2に該当するJsonデータを取得
    request:
                   #↓変わりました
      url: https://{host}/posts/2
      method: GET
    response:
      status_code: 200
      json:
        userId: 1
        id: 2
        title: "qui est esse"
        body: "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
```

こうすることで、{host}がcommon.yamlにある、hostに変わります。
このように外部ファイルを使うことで変数を別の所に宣言することができます。

## このテスト...開発環境でテストしたいけど他の環境ではしたくない場合

テスト項目によっては、開発環境ではテストして、他の環境ではテストしたくない項目があるかもしれません。
そんな問題もTavernは準備してくれています。

```test_*.tavern.yaml
--
test_name: Get server info from slow endpoint
# ↓marksに注目
marks:
  - dev
stages:
  - name: Get info
    request:
      url: "{host}/get-info-slow"
      method: GET
    response:
      status_code: 200
      json:
        n_users: 2048
        n_queries: 10000
---
test_name: Get server info from fast endpoint
# ↓marksに注目
marks:
  - test
stages:
  - name: Get info
    request:
      url: "{host}/get-info"
      method: GET
    response:
      status_code: 200
      json:
        n_items: 2048
        n_queries: 5
```
marksに、「test」と「dev」を追加しました。こうすることで実行コマンドに「test」か「dev」かの情報を載せることが可能になり、markされたものしか実行しなくなります。

実行時

```
↓ marksにdevと付けたものしか実行しません。
$ pytest -m "dev"

↓ marksにtestと付けたものしか実行しません。
$ pytest -m "test"
```

# 終わり
tavernを使った簡単APIテストはまだ記事も少ないので、少しでも参考にしていただけたら幸いです。
また、今回紹介したのは「これでとりあえずテストが進められるかな？」といったものを抽出して書いています。
なので、他にもどんなものがあるか気になったり。特殊なAPIのテストをしたい(ログイン関係など)等あれば、是非公式サイトの方覗いてみてください。

# 参考

[Tavern公式](https://tavern.readthedocs.io/en/latest/index.html)
[Tavern github](https://github.com/taverntesting/tavern)
[【随時更新】一風変わったWeb APIをまとめてみた](https://qiita.com/danishi/items/42d8adf6291515e62284)
[yaml でテストが書ける Tavernというテストツールがすごい](https://qiita.com/flatfisher/items/d8377711a30af1e9856b)