最近 Slack クローンの mattermost のチャットボットを Python で作った。
そのコードをテストしたかったときに unittest を覚えたのでそのメモ。
1
2
| $ python -V
Python 3.6.4 :: Anaconda, Inc.
|
ディレクトリ構成は以下。
1
2
3
4
5
6
| $ tree
.
|____log
| |____mattermost_bot.log
|____mattermostbot.py
|____test_mattermostbot.py
|
mattermostbot.py
: ボット本体
test_mattermostbot.py
: テストコード
log/
: ログ保存用のディレクトリ
チャットボット
mattermost の統合機能から「外向きのウェブフック」を設定する。
チャットボットは bot_name COMMAND ARGUMENT
のように呼び出す。
ここではコマンドは以下の5種類をテストすることにする。
- echo:
ARGUMENT
をそのまま返す
- hoge: “hoge” と返す
- ping: “pong :ping_pong:” と返す
- sushi: “(っ’–‘)╮ =͟͟͞͞ :sushi: ブォン” を返す
- tenki: 今日と明日の天気を返す (Livedoor の天気 API を利用)
チャットボットのコード mattermostbot.py
は以下。Slack のボットとしても機能するはず。
Flask を使って HTTP POST で送られてくる JSON データを mattermost から受け取って、処理している。
bot()
メソッドがボットメイン処理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
| import json
import logging
import requests
from flask import Flask, request
from logging.handlers import RotatingFileHandler
app = Flask(__name__)
@app.route('/bot', methods=['POST'])
def bot():
# mattermost -> bot へ送信される JSON データの取得
post_dict = request.form
app.logger.info(post_dict)
# JSONから token と text (ユーザが入力したメッセージ) を取得
token = post_dict['token']
income_text = post_dict['text']
# income_text は `bot_name COMMAND ARGUMENT` のような形式なので
# 半角スペースで分割し、それぞれの要素を変数に格納する
text_array = income_text.split(' ')
bot_name = text_array[0]
command = text_array[1]
arg = " ".join(text_array[2:])
payload_text = ""
# command によって処理を分岐する
if command == "echo":
payload_text = echo(arg)
elif command == "hoge":
payload_text = hoge()
elif command == "ping":
payload_text = pong()
elif command == "sushi":
payload_text = sushi()
elif command == "tenki":
payload_text = tenki()
app.logger.info(payload_text)
# レスポンス用の JSON を組み立てる
payload = {
'username': bot_name,
'icon_url': 'http://your-server/images/bot_icon.png',
'text': payload_text,
'MATTERMOST_TOKEN': token
}
json_payload = json.dumps(payload)
return json_payload
# ログ出力用メソッド
def log(app):
handler = RotatingFileHandler('log/mattermost_bot.log', maxBytes=10000, backupCount=2)
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s\t%(lineno)d\t%(levelname)s\t%(name)s\t%(message)s')
handler.setFormatter(formatter)
app.logger.addHandler(handler)
# -------------------------------------------------------
# echo command
# -------------------------------------------------------
def echo(text):
return text
# -------------------------------------------------------
# hoge command
# -------------------------------------------------------
def hoge():
return "hoge"
# -------------------------------------------------------
# ping command
# -------------------------------------------------------
def pong():
pong_msg = "pong :ping_pong:"
return pong_msg
# -------------------------------------------------------
# sushi command
# -------------------------------------------------------
def sushi():
return "(っ'-')╮ =͟͟͞͞ :sushi: ブォン"
# -------------------------------------------------------
# tenki command
# -------------------------------------------------------
def tenki():
api_url = "http://weather.livedoor.com/forecast/webservice/json/v1?city="
# 横浜の city id
# その他のIDはここから探して: http://weather.livedoor.com/forecast/rss/primary_area.xml
city_id = "140010"
api_res = requests.get(api_url + city_id)
json_res = json.loads(api_res.text)
today = json_res['forecasts'][0]
tomorrow = json_res['forecasts'][1]
tenki_info = ""
tenki_info += format_tenki_json(today)
tenki_info += format_tenki_json(tomorrow)
return tenki_info
def format_tenki_json(j):
tenki_info = "# "
tenki_info += j['dateLabel'] + " (" + j['date'] + ") の横浜の天気は" + j['telop']
tenki_info += " ![](" + j['image']['url'] + ")\n"
t_min = j['temperature']['min']
t_max = j['temperature']['max']
t_min_str = " -- " if t_min is None else str(t_min['celsius'])
t_max_str = " -- " if t_max is None else str(t_max['celsius'])
tenki_info += "- 最低気温は " + t_min_str +"℃\n"
tenki_info += "- 最高気温は " + t_max_str +"℃\n\n"
return tenki_info
if __name__ == '__main__':
log(app)
app.debug = True
app.run(host='0.0.0.0')
|
テスト
以下を参考にした。
Python の Flask で作ったアプリケーションをテストする | CUBE SUGAR STORAGE
各チャットボットコマンドに対応するテストケースを以下のように書ける。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| import datetime
import json
import mattermostbot
import unittest
class TestMattermostBot(unittest.TestCase):
# mattermost -> bot へのリクエスト JSON のダミーデータ雛形
data = dict(
channel_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
channel_name = 'some_channel',
file_ids = '',
post_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
team_domain = 'some_team',
team_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
text = 'your_bot_name ',
timestamp = '9999999999',
token = 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
trigger_word = 'your_bot_name',
user_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
user_name = 'your_name')
def setUp(self):
self.app = mattermostbot.app.test_client()
def test_post_echo(self):
response = TestMattermostBot.req(self, 'echo fuga')
response_data = json.loads(response.data)
assert response.status_code == 200
assert response_data['text'] == 'fuga'
def test_post_hoge(self):
response = TestMattermostBot.req(self, 'hoge')
response_data = json.loads(response.data)
assert response.status_code == 200
assert response_data['text'] == 'hoge'
def test_post_ping(self):
response = TestMattermostBot.req(self, 'ping')
response_data = json.loads(response.data)
assert response.status_code == 200
assert response_data['text'] == 'pong :ping_pong:'
def test_post_sushi(self):
response = TestMattermostBot.req(self, 'sushi')
response_data = json.loads(response.data)
assert response.status_code == 200
assert response_data['text'] == "(っ'-')╮ =͟͟͞͞ :sushi: ブォン"
def test_post_tenki(self):
response = TestMattermostBot.req(self, 'tenki')
response_data = json.loads(response.data)
assert response.status_code == 200
assert datetime.date.today().strftime('%Y-%m-%d') in response_data['text']
def req(self, command):
data = TestMattermostBot.data.copy()
data['text'] = data['text'] + command
return self.app.post('/bot', data=data)
if __name__ == '__main__':
unittest.main()
|
テストを実行するとこんな感じになる。
1
2
3
4
5
6
| $ python test_mattermostbot.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.115s
OK
|