テストの改善案の案
エンジニアのKPTで「CircleCIで走らせてるテストが項目数の割には30分超えてきたから速くしたい」という話をしたけどちゃんと説明できなかったので書く
環境
問題点
テスト実行の最初(いわゆる @Beforeとかsetup)で、マスタデータをJVMメモリに乗せているが、本番環境と同じデータを使ってる。
いちいち作らなくてもいいというメリットがあるが、本番環境と同じデータなのでいかんせんデータ量が多い。
本番環境のデータをそのまま使うとコード上通らない箇所(フェイルセーフしてる箇所)や、昔動いてたけど今は使わなくなったコードがあるのでカバレッジが上がらない。
あと、テスト用のユーザデータ(マスタデータ)を入れるときに、SQLスクリプトを生で実行したり、ヘルパークラス経由で入れてるので、バラバラになってる
2年ほど前に途中でこのチームに入った時に、テストがなかったので「勢いで」自分が入れたのが敗因だった。もう少し戦略的にやればよかったと反省しているが、入れたことへの後悔はしてない
改善案
本番環境とテスト用のデータを分けて、必要な物だけ入れる また、テストデータがバラバラになっているのはコメントが書けるYAMLに合わせる 例
user: - # ああいうユーザ id: 1 name: 10 birthday: 2016-01-01 - # こういうユーザ id: 2 name: test birthday: 2017-01-01
このファイルを用意しておき、テストを書く人がsetup時に読み込ませる。読み込ませるときには一度databaseをdrop→createした上で読み込ませる。(テストが並列化できないが、そもそもJUnitでは並列化できないからいい)
あとは既存のデータをYAMLへ変換するツールなどを作ればちょっとは楽になるかな?と思ってる。
ちなみにはてブ上で盛り上がったテスト書かなくてもいいよ的な話は、育ってきた環境やフェーズが違うからどちらが正しいとも言えないと思う。
今更サーバサイドとフロントエンドの責務を考えてみる
まえがき
仕事(Webブラウザ上で動くソーシャルゲーム開発)でSPA(Single Page Application)のサーバ側(いわゆるJSONを返すAPIを作成)を担当していた同僚の自分より若いエンジニアと話していたので自分の考えをまとめておきます。あと、ここではAjaxを使った非同期通信の話も混ぜて話してます。というか、話してる途中でそちらの方へ流れた。
なお、ここに書いてあることは、ずっと前から色々なところで話をされていることなので、書いていて今更感満載の内容になったけど、まぁいいかなと。
話の流れ
「やっぱサーバ側はJSON返却するだけだと楽だよね」という話になったので、「まーそうなんだけどねー何でもかんでもフロント側に任せすぎるのもよくないよね」という話になりました 。
フロントはフロントで色々大変なんだから、サーバとフロントの両方の知識を持ってる人が全体をデザインしないと大変になるよねというのが結論です。
前のプロジェクトで感じたこと
以前のプロジェクトでもAngularをつかったSPAのゲームを作っているチームに途中参加していたけど、サーバ側に比べて、フロント側が大変そうだなぁというのを感じていました。
というのも、1枚のHTMLページを表示した後にサーバ側と複数のAPIを呼び出して、通信が成功/失敗した時のルーティング処理をして、ブラウザ上にキャラをゴリゴリ動かして、また通信して、返ってきたJSONデータを元にまたキャラをグリグリ動かす。みたいなことをしてました。
ちゃんとした(と言っていいかわからないけど)SPAのサイトは初めてだったけど、開発する人の負荷やブラウザへの負担をかけすぎだよなーというのを感じたのをはっきりとおぼえてます。(フロントエンジニアたちはどうおもってるかは知らない)
自分だったらどうするか?
キャラをゴリゴリ動かしたりするView側の複雑な動きはフロント側の仕事だと思うので、そこはサーバ側はJSONを返すだけにしたいです。
でも、最初に表示するデータを返す箇所はサーバ側でHTML組み立ててもいいと思ってます。というか、無理に全体をSPAにすることにこだわりを持ってないです。
うちのチームだとサーバ側でHTMLを組み立てるようにしても、ERBやfreemarkerなどのテンプレートエンジン上にHTMLを書いていくのはフロントエンジニアの仕事だからなのですがw
また、細かいルーティングなどは開発する人、ブラウザへの負担とかを考慮するとサーバ側でやった方がいいかなと思ってます。
よくあるのが軽く見せるために周りのコンテンツを表示してから、Ajaxを使ってメインのコンテンツを表示することがよくあります。あれに若干のギャップを感じていて、体感はあがるのでそこはわかるんだけど、結局メインコンテンツを見せないと意味ないんだよなぁという思いもあります
また、サブコンテンツ(例えば、ボタンをタップしたらお知らせ情報を表示するとか、キャンペーンバナーとか)はAjaxで持ってきたほうが、ユーザの体感はあがるので積極的に使ったほうがいいなと思います。(ただしリクエスト数には注意)
最後にデータを更新して、その結果を使って何かする(ソシャゲでよくあるボタンタップしてクエストを進める)ものは都度全部の画面描画する必要はないので、Ajax通信で完結させたほうがいいかなと思います
蛇足
前のプロジェクトで管理画面がデータの取得にもAjax通信を使ってサーバとやり取りしていてだいぶ使いづらかったです。
他のメンバーとURLを共有することはよくあるので、ユーザが一意のURLをわかるようにしてあげるといいなと思いました(小並感
ロードス島戦記に学ぶ技術的負債の返し方
1年前にいたプロジェクトに戻ってきて3週間ほど経過しました。
ミーティングが多くてガッツリコードを書くことがなくなりました。
同じフロアにいたため時々飲みに誘ってもらってたので久しぶり感はなかったのですが、コードを見るとだいぶ変わってました。
1クール目はJava8を入れ逃げしたので、ちゃんと使ってくれてるのは嬉しかったです。
サービスを2年ぐらいやっているのでよくも悪くも色々なものが溜まってました。
いわゆる技術的負債です。
技術的負債は今から見ると負債になっているのですが、その頃は時間的、技術的制約でそれがベターだと判断されたものに加えて、運用中に状況が変わって作った時の想定と異なる使われた方をしたとか、大量のデータが蓄積することで発生する問題とか色々あります。
例えば
- 当初は10連ガチャ想定だったのでinsert文を10回発行で運用できていたが、100連ガチャになったので最大100回のinsertが走るようになった カードアイテムはまとめて付与というのは仕様的に難しい bulk insertを使いたかったが、テーブル分割をしていたためORMマッパーが対応してなかった
- 運用で溜まったマスタデータが8万件になって、それをすべてアプリ起動時にメモリに入れてる。そのためアプリ起動に時間がかかる ** ローカル環境やCircleCIでのテスト実行時にも本番と同じマスタを使っているのでそれぞれにも時間がかかってる
スタート時はまずユーザにサービスを提供してナンボというところはあるのでしょうがないですね。 全部を綺麗に作りすぎてたら出すタイミングを逃してサービスが終わったなんて本末転倒。 ただ、もうやらないといけない時期かなという判断をしたので改善していきたいなと。何より自分の精神衛生上良くない 最大の懸念は改善をどうやってしていくかですね。パターンはいくつかあると思います。
- チーム内外から改善する専用の人を設ける
- 週1日の改善dayを設けてみんなで改善する
- 気づいた人が随時改善する
どうしようかなーと思ってた矢先に10年以上ぶりにロードス戦記を読んでて傭兵王カシューが治めるフレイム国の軍隊の運用がうまいなーと感心。 そこは正規の騎士団と傭兵団をうまく使い分けてて、きっちり固めに正攻法に真正面から行く時は騎士団で、変則的な相手や柔軟にいきたいときは自由に動ける傭兵団で運用してました。 毎週のようにリリースがあり施策ごとにスケジュールがバラバラでメンバーによって得手不得手があるので、みんなで改善するよりも、ミーティングが多くてフルで開発に入れない自分が最初に流れを作りつつ気づいた人がやっていくというのが一番無難な流れかなと最近思ってます。
新装版 ロードス島戦記 3 火竜山の魔竜(上)<ロードス島戦記> (角川スニーカー文庫)
- 作者: 水野良
- 出版社/メーカー: KADOKAWA / 角川書店
- 発売日: 2013/12/02
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
新装版 ロードス島戦記 4 火竜山の魔竜(下)<ロードス島戦記> (角川スニーカー文庫)
- 作者: 水野良
- 出版社/メーカー: KADOKAWA / 角川書店
- 発売日: 2013/12/28
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
次のソシャゲ開発でやったほうがいいなと思うこと
チュートリアル駆動開発
- チュートリアルがユーザが一番最初に見るところだから、そこで落ちると意味が無い
- チームとしてはゲームの幹をはっきりするため
- ここにないものは実装するかどうか検討する
イテレーション0を大切にする
- Jenkins、ステージング環境、デプロイとか
- 最低でもステージング環境までは作る
チーム内レビューのクオリティをあげる
- エラーになるのは論外
- ボタンなどの素材もできる限り本番に出すものと同じものを。(カード素材は難しいかも)
前イテレーションで終わらなかったものを次イテレーションと平行させない
- というか、あまり作業を平行させない
- 脳みそのスイッチコスト高い
- クオリティ落ちる
- 疲弊する
- 最初から頑張るではなくて何か対策を打つ。無理に頑張ってもクオリティは落ちる。頑張るは思考停止
- ただし、最後の最後は踏ん張る
ターゲットとしているユーザのペルソナを作る
- 年齢とかどういうゲームが好きかとか職業とか、プレイする時間帯とか
- 暗黙の了解にしてる部分があるので明確化する
- 萌えゲーだと野郎ばっかりだから考えてて楽しいかは別問題
◯◯ゲームのような感じという言葉を可能な限り使わない
- ◯◯ゲームと△△ゲームという感じみたいにして組み合わせてもつぎはぎ感がある
- 似た要素があってはいけないというわけではない。トータルのバランスが大事
- そもそもレッドオーシャン(どころかブラッドオーシャン)だから、似たもの出してもあんまりヒットしないよね
テーマを決める
- テイルズ系みたいにちゃんとゲームのキャッチフレーズを決めたい
- それによってゲームのトーンとか変わると思う
他にもなにかあったと思うけど忘れた 思い出したので3つを追記した
Node.jsからTwitterAPIを使う
Node.jsからTwitterAPIを使ってみたのでまとめました
やりたいこと
- TwitterAPIを使ってツイート内容と画像をあげる
- WebブラウザからのアクセスとUIWebViewを使ったアプリから使われるのでPin認証を使う
手順
- Twitter Application Management からアプリ作成する。Settingsタブの"Callback URL"は空欄にする。いつの間にか携帯電話番号が必要になっていた
npm install oauth twitter --save
でライブラリをインストール- 軽く検証するときは、アプリの設定画面のKey and Access TokensタブのYour Access TokenでIDを発行する
あとはGistのような感じで。
コードの補足
L45でTwitterオブジェクトをnew するときに _.merge(clientConfig,...
をしているのは、テストを書くときにnockを使ってテストをしたいため。テスト時にはconfig部分を変更している。
処理の流れとしては
- getOAuthTokenでトークンとPinを表示するためのURLを取得
- クライアント側でTwitterのPinの画面からコピーしたpin番号を渡してもらってauthCallbackを呼び出す。
- uploadMediaをアップしたい画像の数分呼び出す
- tweet処理でツイート内容とuploadMediaのcallbackで取得した
media_id_string
のカンマ区切りをわたす
感想
ドキュメントしっかりしてるのと、npmにライブラリが揃ってるから簡単に実装できた(小並感)
2015-07-07 画像周りの追記
ツイート内容に決まってる画像を埋め込む場合は、いちいちアップするのは通信の無駄なので、
- 鍵がかかってないアカウントに画像をUPする
- UPしたツイートのリンクから画像のURLをとる(例えば:pic.twitter.com/wldYWc3CXn)
- 画像のURLを本文に埋め込む
参考
- PIN-based authorization | Twitter Developers
- OAuth Core 1.0
- API Rate Limits | Twitter Developers
- Uploading Media | Twitter Developers
- POST statuses/update | Twitter Developers
コード
S3とCloud Storageのアップロード時間が全然違った話
S3とCloud Storageのファイル転送速度どのぐらいちがうのかなーと思って計測してみました。 2KBぐらいの5000枚の画像ファイルを作ってそれをインスタンス→S3、cloud StorageとS3(cloud Storage)→S3(cloud Storage)で計測してみました。 多分、ネットワークとか色々変動したりすると思うので、導入時に検証したほうがよいかと思うけど、参考までに。
#!/bin/sh set -eu mkdir $HOME/image for j in `seq 1 100` do cd $HOME/image mkdir $j && cd $j for i in `seq 1 50` do convert -size 1024x640 xc:yellow -pointsize 20 -draw "text 0,20 test_$i" test_$i.png done done
AWS
使ったコマンド
# ローカルからバケット date; aws s3 sync . s3://<foo>/`date +"%Y%m%d%H%S"` --exclude '*' --include 'image/*' --quiet; date # バケットからバケット date; aws s3 sync s3://<foo>/201506070403 s3://<foo>/`date +"%Y%m%d%H%S"` --exclude '*' --include 'image/*' --quiet; date
- インスタンス→S3 36秒
- S3→S3 44秒
# インスタンス→S3にかかった時間 2015年 6月 7日 日曜日 12:20:31 UTC 2015年 6月 7日 日曜日 12:21:07 UTC # S3→S3にかかった時間 2015年 6月 7日 日曜日 12:21:36 UTC 2015年 6月 7日 日曜日 12:22:21 UTC
GCP
使ったコマンド
date; gsutil -m rsync -d -R image gs://<foo>/`date +"%Y%m%d%H%M"`/image ; date date; gsutil -m rsync -d -R gs://<foo>/201506070403/image gs://<foo>/`date +"%Y%m%d%H%M"`/image ; date
- インスタンス→Cloud Storage 約3分
- Cloud Storage→Cloud Storage 約8分
# インスタンス→Cloud Storageにかかった時間 2015年 6月 7日 日曜日 12:02:09 UTC 2015年 6月 7日 日曜日 12:05:05 UTC # Cloud Storage→Cloud Storageにかかった時間 2015年 6月 7日 日曜日 12:13:37 UTC 2015年 6月 7日 日曜日 12:21:18 UTC
Sinon.JSのuseFakeTimersを教えてもらった
全然関係ないローカルテスト通らないと調べてたら、テスト全体を一定時間内に終わらないとfailするテストだった…マジか orz
— Eiji Hachiya (@hachi_eiji) 2015, 5月 18
説明難しいんだけど マスタの開始日と終了日が現在日付〜現在日付+200秒になってて、このデータを取得するための基準点が現在日付+150秒となってた。ということは、テスト開始してから50秒以内にテストを始めないといけない。という罠
— Eiji Hachiya (@hachi_eiji) 2015, 5月 18
ということがあって、HipChat上に投げたら、sinon.useFakeTimersつかってないの?というツッコミがあったので、そんなのあるんだと。ちょっと使ってみました。 あら便利
http://sinonjs.org/ を見てみると JavaScript test spies, stubs and mocks.らしい。ちなみにバージョンは1.14.1で動かしてます。
var sinon = require('sinon'); describe('test', function() { describe('sub-test', function() { var sandbox; beforeEach(function(done) { sandbox = sinon.sandbox.create(); // sandbox でまとめて管理 sandbox.useFakeTimers(new Date(2000, 0, 1).getTime()); done(); }); it('call new Date', function(done) { console.log(new Date()); done(); }); it('call new Date2', function(done) { console.log(new Date()); done(); }); afterEach(function(done) { sandbox.restore(); console.log('afterEach', new Date()); done(); }); }); });
で動かすと、
test sub-test Sat Jan 01 2000 00:00:00 GMT+0900 (JST) ✓ call new Date afterEach Thu May 21 2015 23:02:23 GMT+0900 (JST) Sat Jan 01 2000 00:00:00 GMT+0900 (JST) ✓ call new Date2 afterEach Thu May 21 2015 23:02:23 GMT+0900 (JST) 2 passing (14ms)