概要
- Common Testでcowboyを使って書いたサーバの結合テストを書きたい
- 結合テストだけどcowboy handlerのStateを検証したい
case application:get_application(ct) of...
でCommon Test時のみ有効になるAPIを追加して検証することができる- もっと良い方法があるかもしれない…
Common TestでWSサーバの結合テストを書く
cowboyを使ったサーバを書いています。
moduleの単体テストだけではなく、WSサーバならWS handshakeしてからのframeの送受信を検証する結合テストも書くとより安心です。
私は、結合テストをCommon Testで力技で書きました。
WSサーバのクライアントとしてgunを使っています。 また、WS frameのフォーマットとしてJSONを使っており、エンコードとデコードにjsxを使っています。
init_per_testcase(_, Config) ->
{ok, Started} = application:ensure_all_started(my_server),
{ok, Gun} = application:ensure_all_started(gun),
[{gun, Gun}, {applications, Started} | Config].
WS frameを送ったあとの検証はこんな感じでかけます。
%% テストケース
should_get_pong_when_send_ping(Config) ->
{ConnPid, StreamRef} = connect(Config),
gun:ws_send(ConnPid, {text, jsx:encode(ping())}),
receive {gun_ws, ConnPid, {text, Msg} = Frame} ->
ct:pal("Got msg: ~p", [Frame]),
true = jsx:encode(pong()) =:= Msg
after 500 ->
ct:fail(timeout)
end,`
{comment, "PINGを送ったらPONGが返ること"}.
%% helper
connect(Config) ->
{ok, ConnPid} = gun:open("localhost", 8080),
StreamRef = gun:ws_upgrade(ConnPid, "/ws", [
%% additional headers
]),
receive
{gun_ws_upgrade, ConnPid, ok, Headers} ->
{ConnPid, StreamRef};
{gun_response, ConnPid, _, _, Status, Headers} ->
exit({ws_upgrade_failed, Status, Headers});
{gun_error, ConnPid, StreamRef, Reason} ->
exit({ws_upgrade_failed, Reason})
after 1000 ->
exit(timeout)
end.
結合テスト用のAPIを用意する
あるframeを送って、その結果がframeとして受信できるようなAPIであれば、上記のようなケースを書けばいいので簡単です。 ですが、単にhandlerのstateが更新されるようなケースをテストしたい場合がありました。
簡単に検証するために、
- handlerにCommon Test実行時にのみ有効になるAPIを用意する
という方法をとって、テストケースをつくりました。
cowboyのwebsocket handlerのwebsocket_handle
関数内でctが稼動しているか検証し、稼動していたらテスト時のみ有効にする挙動を書きます。
websocket_handle({text, Msg}, Req, State) ->
Result = case application:get_application(ct) of
{ok, common_test} ->
%% テスト用の処理(API)
_ ->
%% 通常利用時の処理
end,
{reply, {text, jsx:encode(Result)}, Req, NewState}.
テストケースでは、WSを使ってテスト時のみ動くAPIを実行します。
gun:ws_send(ConnPid, {text, jsx:encode(#{ <<"test">> => <<"getState">> })}),
receive {gun_ws, ConnPid, {text, Msg} = Frame} ->
#{ <<"state">> := #{ <<"user_id">> := Actual } } = jsx:decode(Msg, [return_maps]),
true = Actual =:= Expected
after 500 ->
ct:fail(timeout)
end
このようにして半ば無理やりにStateを検証することができます。
ただ、プロダクションコードにテスト時のみ使うAPIが入ってしまうのが、悩ましいところです。もっと良い方法があったら知りたいです。