このエントリーは、KLab Advent Calendar 2015 の12/4の記事です。
KLabとしては久々のAdvent Calendar参戦です。4番手も緊張しますね。@hasi_tです。よろしくお願いします。
今年は、PlaygroundとLuaによる大規模モバイルオンラインゲーム開発のレベルアップという題でCEDEC2015で発表することができました。
スライドと原稿を公開したので、よければご覧ください。
この記事では、「Modelとサーバを統合したテスト」を支えるLunatic Pythonを紹介しようと思います。
使用した言語のバージョンは、Python 3.4とLua 5.2です。
Lunatic PythonはPythonとLuaの双方向ブリッジで、PythonからLuaを呼び出して、さらにその中からPythonを呼び出し、さらにその中からLuaを呼び出し、…することができます。
https://github.com/bastibe/lunatic-python にあるものを用います。
"Sadly, Lunatic Python is very much outdated and won't work with either a current Python or Lua." とあるように、
オリジナルのほうは古くなっているので注意が必要です。
Playgroundゲームエンジンでは、ゲームを記述するプログラミング言語としてLuaを用います。
そして、ここではPythonで書かれたHTTPサーバと通信するゲームを考えます。
サーバに閉じたテストは比較的自動化しやすいのですが、クライアント・サーバを統合したテストは、サーバを立てて実際にクライアントアプリを実行してテストするのが従来の方法でした。
従来の方法だと自動化が難しいのが問題です。
そこでまず、クライアントサイドのLuaコードを、Playgroundゲームエンジンに依存する部分と依存しない部分に分けます。
更に、Playgroundゲームエンジンに依存しない部分のうち、通信に関わる部分をクライアント・サーバ統合テストの対象とします。(下図)
Lunatic Pythonが提供する機能はとても単純なので、ラッパーオブジェクトを作って書きやすくしようと思います。
Lua5.2で導入されたenvironmentを使って、以下のようにLuaObject
というクラスを定義します。
class LuaObject:
def __init__(self):
self.env = lua.eval('{}')
def run_lua_code(self, ld, src='_', **kw):
return lua.eval('''
load(python.locals().ld, python.locals().src, 't', setmetatable({}, {
__index = function(_, key)
local w = python.locals().kw[key]
if w ~= nil then return w end
local v = python.locals().self.env[key]
if v ~= nil then return v end
return _G[key]
end,
__newindex = function(_, key, value)
if python.locals().kw[key] ~= nil then error('kw is read only') end
python.locals().self.env[key] = value
end,
}))()
''')
例えば、下のコードを実行すると4が出力されます。
L = LuaObject()
L.run_lua_code('x = 2 + 2')
print(L.run_lua_code('return x')) # 4が出力される
environmentはインスタンスごとになっているので、グローバル変数への書き込みはそのインスタンスだけに影響します。
L1 = LuaObject()
L2 = LuaObject()
L1.run_lua_code('x = 1')
L2.run_lua_code('x = 2')
print(L1.run_lua_code('return x')) # 1が出力される
print(L2.run_lua_code('return x')) # 2が出力される
当然、普通にluaのライブラリ関数を使うこともできます。
print(L1.run_lua_code('return string.rep("hoge", 2)')) # hogehogeが出力される
また、キーワード引数を使って簡単に値を渡すことができるようにしています。
L = LuaObject()
print(L.run_lua_code('return y', y=10)) # 10が出力される
print(L.run_lua_code('return y')) # Noneが出力される
こうすることで、複数のクライアントを並行して動作させるテストも簡単に書けるようになりました。
次に、Luaからサーバと通信する部分を作ります。
といっても、Pythonの中でLuaスクリプトが動いているので、実際に通信することはなく通信に関するテストを実現できます。
例として、以下のようなJSONでデータをやりとりするFlaskを使ったAPIサーバを考えます。
def get_app():
app = flask.Flask(__name__)
@app.route('/incr', methods=['POST'])
def incr():
request_data = json.loads(flask.request.data.decode('utf-8'))
return flask.jsonify(n=request_data['n'] + 1)
return app
pytestとWebTestを使って、以下のようにテストを書くことができますが、これはサーバに閉じたテストです。
def test_api():
app = get_app()
test_app = webtest.TestApp(app)
response = test_app.post(
url='/incr',
params='{"n":1}',
headers=[('Content-Type', 'application/json')],
)
assert response.json['n'] == 2
ここで、以下のようにLuaClient
というクラスを定義します。
class LuaClient(LuaObject):
def __init__(self, test_app):
super().__init__()
self.test_app = test_app
self.run_lua_code('''
function json_encode(data)
if type(data) == 'table' then
local a = {}
local r = {}
for i, v in ipairs(data) do
table.insert(a, json_encode(v))
end
for k, v in pairs(data) do
table.insert(r, json_encode(tostring(k))..':'..json_encode(v))
end
if #a == #r then
return '['..table.concat(a, ',')..']'
else
return '{'..table.concat(r, ',')..'}'
end
elseif type(data) == 'string' then
return python.import('json').dumps(data, false, false)
else
return tostring(data)
end
end
function call_api(path, data, callback)
local res = python.locals().self.call_api(path, json_encode(data))
if res then
callback(res)
end
end
''')
def call_api(self, path, data):
response = self.test_app.post(
url=path,
params=data,
headers=[('Content-Type', 'application/json')],
)
def json_to_lua(x):
if isinstance(x, dict):
res = lua.eval('{}')
for k, v in x.items():
res[k] = json_to_lua(v)
return res
elif isinstance(x, list):
res = lua.eval('{}')
for i, v in enumerate(x, start=1):
res[i] = json_to_lua(v)
return res
else:
return x
return json_to_lua(response.json)
LuaClient
を使うことで、Luaからサーバを呼び出すテストを以下のように書くことができます。
def test_incr():
app = get_app()
test_app = webtest.TestApp(app)
client = LuaClient(test_app)
client.run_lua_code('''
call_api('/incr', {n = 1}, function(data)
response = data
end)
''')
assert client.run_lua_code('return response.n') == 2
ここでは簡単のためにプレイヤー作成や認証に関する処理を省いていますが、
プレイヤーを作成しデータベースを書き換えてログインさせるといったテストも簡単に書くことができます。
データベース書き換えは、クライアントテストを拡張してクライアント・サーバ統合テストを実現しようとしたときに面倒だったところで、
これはサーバテストを拡張したクライアント・サーバ統合テストの長所といえます。
明日はmecha_g3氏が何か書くそうです。お楽しみに。
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。